diff --git a/package.json b/package.json
index 682ef16..3e2fd0a 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"dev": "next dev",
"prebuild": "node scripts/generate-sitemap.mjs",
"build": "next build",
+ "postbuild": "node scripts/inject-structured-data.mjs",
"start": "next start",
"lint": "eslint"
},
diff --git a/public/feed.xml b/public/feed.xml
index 3c80684..0d82743 100644
--- a/public/feed.xml
+++ b/public/feed.xml
@@ -62,6 +62,27 @@
Wed, 07 Oct 2026 00:00:00 GMTA complete map of the 77 apps DashCaddy can deploy in one click — media, files, photos, DNS, security, databases, automation and more, each with its own install guide.
+
+ Self-Host Your Photo Library: Immich in 10 Minutes
+ https://dashcaddy.net/blog/self-host-your-photo-library/
+ https://dashcaddy.net/blog/self-host-your-photo-library/
+ Wed, 07 Oct 2026 00:00:00 GMT
+ Stop paying a monthly subscription to look at your own photos. Deploy Immich with one click, put it behind HTTPS, and get automatic phone backup onto hardware you own.
+
+
+ Self-Host Vaultwarden in 10 Minutes (Your Own Password Manager)
+ https://dashcaddy.net/blog/self-host-vaultwarden/
+ https://dashcaddy.net/blog/self-host-vaultwarden/
+ Wed, 07 Oct 2026 00:00:00 GMT
+ Run your own Bitwarden-compatible password manager on your own server: install Vaultwarden with one click, put it on HTTPS, and keep your vaults off someone else's cloud.
+
+
+ Getting Started with DashCaddy: From Blank Server to a Running App
+ https://dashcaddy.net/blog/getting-started-with-dashcaddy/
+ https://dashcaddy.net/blog/getting-started-with-dashcaddy/
+ Wed, 07 Oct 2026 00:00:00 GMT
+ A step-by-step walkthrough: pick a server, install DashCaddy with one command, open the dashboard, deploy your first self-hosted app, and get it on HTTPS.
+ Homelab Monitoring: Know When a Docker Service Goes Down
https://dashcaddy.net/blog/homelab-monitoring-guide/
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 37da2ce..5cd88fd 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -51,6 +51,12 @@
monthly0.8
+
+ https://dashcaddy.net/blog/getting-started-with-dashcaddy/
+ 2026-10-07
+ monthly
+ 0.8
+ https://dashcaddy.net/blog/homelab-beginners-guide/2026-10-06
@@ -75,6 +81,18 @@
monthly0.8
+
+ https://dashcaddy.net/blog/self-host-vaultwarden/
+ 2026-10-07
+ monthly
+ 0.8
+
+
+ https://dashcaddy.net/blog/self-host-your-photo-library/
+ 2026-10-07
+ monthly
+ 0.8
+ https://dashcaddy.net/blog/self-hosted-app-catalog-guide/2026-10-07
diff --git a/scripts/inject-structured-data.mjs b/scripts/inject-structured-data.mjs
new file mode 100644
index 0000000..1091ad8
--- /dev/null
+++ b/scripts/inject-structured-data.mjs
@@ -0,0 +1,492 @@
+#!/usr/bin/env node
+/**
+ * inject-structured-data.mjs
+ *
+ * Adds JSON-LD structured data to every built DashCaddy app-guide page
+ * (out/docs/catalog//index.html) after `next build` has produced the
+ * static export.
+ *
+ * Design rules:
+ * - Runs on npm `postbuild`, so it operates on the real rendered HTML, not on
+ * the TSX source. Everything it emits is therefore provably derived from
+ * text a visitor can already see on the page (Google's requirement for
+ * FAQPage / HowTo markup).
+ * - Emits nothing it cannot ground. A page with no category badge gets no
+ * applicationCategory; a page with no troubleshooting list gets no FAQPage;
+ * a page with no dashboard install steps gets no HowTo. Missing fields are
+ * reported, never invented.
+ * - Idempotent: an already-injected page is detected and skipped.
+ *
+ * Never hand-edit this file's OUTPUT. Re-run the build.
+ */
+
+import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const REPO = join(HERE, "..");
+const OUT = join(REPO, "out");
+const CATALOG = join(OUT, "docs", "catalog");
+
+const ORIGIN = "https://dashcaddy.net";
+const MARKER = 'data-dashcaddy-jsonld="app-guide"';
+const BLOG_MARKER = 'data-dashcaddy-jsonld="blog-breadcrumb-faq"';
+
+/* ------------------------------------------------------------------ text --- */
+
+const ENTITIES = {
+ amp: "&",
+ lt: "<",
+ gt: ">",
+ quot: '"',
+ apos: "'",
+ nbsp: " ",
+ "39": "'",
+ "x27": "'",
+ "x2F": "/",
+ "47": "/",
+};
+
+function decodeEntities(s) {
+ return s.replace(/?([a-zA-Z0-9]+);/g, (whole, code) => {
+ if (Object.prototype.hasOwnProperty.call(ENTITIES, code)) return ENTITIES[code];
+ if (Object.prototype.hasOwnProperty.call(ENTITIES, "#" + code)) {
+ return ENTITIES["#" + code];
+ }
+ if (/^#?\d+$/.test(code)) {
+ const n = Number(code.replace("#", ""));
+ if (Number.isFinite(n) && n > 0 && n < 0x10ffff) return String.fromCodePoint(n);
+ }
+ if (/^#?x[0-9a-f]+$/i.test(code)) {
+ const n = parseInt(code.replace(/^#?x/i, ""), 16);
+ if (Number.isFinite(n) && n > 0 && n < 0x10ffff) return String.fromCodePoint(n);
+ }
+ return whole;
+ });
+}
+
+/** Strip tags, decode entities, collapse whitespace. */
+function text(html) {
+ return decodeEntities(
+ html
+ .replace(/ /gi, " ")
+ .replace(/<[^>]+>/g, " "),
+ )
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+/** Tags stripped / entities decoded for the WHOLE document — used to prove that
+ * anything we mark up is genuinely visible to a visitor. */
+function visibleText(doc) {
+ return text(
+ doc
+ .replace(/\n`;
+
+ if (!html.includes("")) {
+ report.missing.push(`${appId}: no `);
+ continue;
+ }
+ html = html.replace("", `${block}`);
+ writeFileSync(file, html);
+ report.injected += 1;
+ }
+
+ console.log("inject-structured-data: app guide pages");
+ console.log(` pages_seen=${report.pages}`);
+ console.log(` pages_injected=${report.injected}`);
+ console.log(` pages_skipped_already=${report.skipped}`);
+ console.log(` pages_with_faqpage=${report.faq}`);
+ console.log(` pages_with_howto=${report.howto}`);
+ console.log(` pages_with_docker_image=${report.image}`);
+ console.log(` pages_with_gaps=${report.missing.length}`);
+ console.log(` ungrounded_strings_dropped=${report.ungrounded}`);
+ for (const m of report.missing) console.log(` gap: ${m}`);
+
+ if (report.injected + report.skipped !== report.pages) {
+ console.error("inject-structured-data: not every page was injected or skipped");
+ process.exit(1);
+ }
+
+ injectBlogPosts();
+}
+
+/* ------------------------------------------------------------- blog pass --- */
+
+function injectBlogPosts() {
+ const blogDir = join(OUT, "blog");
+ if (!statSync(blogDir, { throwIfNoEntry: false })) {
+ console.log("inject-structured-data: no out/blog dir — skipping blog pass");
+ return;
+ }
+
+ const slugs = readdirSync(blogDir).filter((d) => {
+ try {
+ return statSync(join(blogDir, d)).isDirectory();
+ } catch {
+ return false;
+ }
+ });
+
+ let seen = 0;
+ let injected = 0;
+ let skipped = 0;
+ let withFaq = 0;
+ let questions = 0;
+ let ungrounded = 0;
+ const gaps = [];
+
+ for (const slug of slugs) {
+ const file = join(blogDir, slug, "index.html");
+ if (!statSync(file, { throwIfNoEntry: false })) {
+ gaps.push(`${slug}: no index.html`);
+ continue;
+ }
+ seen += 1;
+ let html = readFileSync(file, "utf8");
+ if (html.includes(BLOG_MARKER)) {
+ skipped += 1;
+ continue;
+ }
+
+ const vis = visibleText(html);
+ const faqs = extractBlogFaqs(html).filter((f) => {
+ if (vis.includes(f.question) && vis.includes(f.answer)) return true;
+ ungrounded += 1;
+ return false;
+ });
+ if (faqs.length >= 2) {
+ withFaq += 1;
+ questions += faqs.length;
+ } else {
+ gaps.push(`${slug}: no visible FAQ section`);
+ }
+
+ const json = JSON.stringify(buildBlogGraph(slug, html, faqs))
+ .replace(//g, "\\u003e")
+ .replace(/&/g, "\\u0026");
+ const block = `\n`;
+
+ if (!html.includes("")) {
+ gaps.push(`${slug}: no `);
+ continue;
+ }
+ writeFileSync(file, html.replace("", `${block}`));
+ injected += 1;
+ }
+
+ console.log("inject-structured-data: blog posts");
+ console.log(` posts_seen=${seen}`);
+ console.log(` posts_injected=${injected}`);
+ console.log(` posts_skipped_already=${skipped}`);
+ console.log(` posts_with_faqpage=${withFaq}`);
+ console.log(` faq_questions=${questions}`);
+ console.log(` ungrounded_strings_dropped=${ungrounded}`);
+ console.log(` notes=${gaps.length}`);
+ for (const g of gaps) console.log(` note: ${g}`);
+}
+
+main();
\ No newline at end of file
diff --git a/src/content/blog/getting-started-with-dashcaddy.md b/src/content/blog/getting-started-with-dashcaddy.md
new file mode 100644
index 0000000..6a5c208
--- /dev/null
+++ b/src/content/blog/getting-started-with-dashcaddy.md
@@ -0,0 +1,139 @@
+---
+title: "Getting Started with DashCaddy: From Blank Server to a Running App"
+description: "A step-by-step walkthrough: pick a server, install DashCaddy with one command, open the dashboard, deploy your first self-hosted app, and get it on HTTPS."
+date: "2026-10-07"
+author: "DashCaddy Team"
+tags: ["dashcaddy getting started", "self-host apps", "docker deployment", "beginner homelab", "self-hosting tutorial"]
+readingMinutes: 7
+---
+
+Self-hosting means running software on hardware you control. The part that stops most people is everything around the app: the container, the reverse proxy, the DNS record, the certificate. A self-hosting platform handles that layer so installing a password manager becomes one decision instead of twenty. [What a self-hosting platform is](/blog/self-hosting-platform-guide/) covers the bigger picture.
+
+This walkthrough takes you from a bare Linux box to your first app running behind a real HTTPS URL, using Vaultwarden as the example because it is small, useful, and a good first pick.
+
+## What DashCaddy does for you
+
+DashCaddy is a control plane for Docker applications. When you deploy a service, it creates the container, writes the Caddy reverse-proxy route, creates the DNS record, issues the HTTPS certificate, and starts health checks. Apps get a subdomain and a padlock without you touching a config file. See the [overview](/blog/what-is-dashcaddy/) for the layers; here we get one running.
+
+## What you need before you start
+
+- **A Linux host that stays on.** Ubuntu 22.04 LTS, Debian 12, or any recent distribution. A spare mini-PC, an old desktop, or a cheap VPS works. **2 GB of RAM and a few GB of disk** is enough.
+- **Root or sudo access.** The installer writes to `/etc/dashcaddy`, `/opt/dockerdata`, and the Docker daemon.
+- **Ports 80 and 443 reachable** if you want public HTTPS.
+- **A domain, if you want automatic DNS and SSL.** A real domain gets a trusted certificate automatically; a custom TLD like `dashcaddy.home` works on your LAN instead.
+- **Docker does not have to be installed already.** If it is missing, the installer adds Docker Engine and the Compose plugin.
+
+## Install DashCaddy with one command
+
+On the host you want to use, run:
+
+```bash
+curl -fsSL https://get.dashcaddy.net | bash
+```
+
+Answer the prompts (domain, install location, storage size); the defaults are sensible. The installer runs seven idempotent steps; re-running it reconciles an existing install rather than clobbering it:
+
+1. Detects your environment (OS, package manager, disk, memory, IP addresses)
+2. Installs Docker Engine and the Compose plugin if missing
+3. Writes configuration to `/etc/dashcaddy` and `/opt/dockerdata`
+4. Builds the DashCaddy container image
+5. Starts the API container, `dashcaddy-api`, on port 3001
+6. Waits for the API to answer its `/healthz` check
+7. Prints the dashboard URL
+
+If a step fails, it stops and tells you what to re-run. The [installation guide](/docs/installation/) covers the details.
+
+### The scripted install variant
+
+To install without prompts, pass your domain inline:
+
+```bash
+curl -fsSL https://get.dashcaddy.net | bash -s -- --domain dashcaddy.example.com
+```
+
+This is the non-interactive form; you can pass a custom TLD or a quick local address instead.
+
+## Open the dashboard for the first time
+
+Open the URL the installer printed. The docs use `https://dashcaddy.home` as the default dashboard address, but the hostname is configurable, so yours may differ.
+
+On first visit you land on the setup wizard, which runs once and saves your choices:
+
+1. **Pick a configuration profile** (*Professional Home Lab*, *Simple*, or *Public* for an internet-exposed VPS)
+2. **Configure your network** (your domain and the LAN IP used for routing)
+3. **Set storage limits** (a cap on Docker data and free-space headroom)
+4. **Review your choices**
+5. **Confirm disk safety**
+
+Then create your first admin account and enroll a TOTP authenticator. Store the recovery codes somewhere safe.
+
+## Deploy your first app: Vaultwarden
+
+Vaultwarden is a lightweight, Bitwarden-compatible password manager. It is a good first deployment: a single small container in the Security category, no media library or host paths required, and immediately useful. It follows the same path any template does, which [one-click Docker deployment](/blog/one-click-docker-deployment/) describes in general terms.
+
+### Deploy it from the dashboard
+
+1. Sign in, then click the **App Selector** button on the dashboard home page.
+2. Pick **Vaultwarden** from the Security category. The template uses the image `vaultwarden/server:latest`.
+3. Fill in the deployment form: **subdomain** (the suggested default is `vault`) and **host port** (the default is `8088`).
+4. Click **Deploy**.
+
+DashCaddy pulls the image, creates the persistent volume, writes the Caddy route, creates the DNS record, and waits up to **30 seconds** for the container's health check on `/` to pass. It is atomic: if a step fails, the deployment rolls back rather than leaving a half-wired service. Other templates follow the same flow, documented in [deploy your first service](/docs/first-service/).
+
+When the service card shows **Running**, the URL becomes clickable. It is built from your subdomain and configured TLD, so a host using the `.home` TLD gives you `https://vault.home`.
+
+### Deploy it from the API instead
+
+Everything the dashboard does is also a REST call:
+
+```bash
+curl -X POST https://dashcaddy.home/api/v1/apps/deploy \
+ -H "X-API-Key: $DASHCADDY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "appId": "vaultwarden",
+ "config": { "subdomain": "vault", "port": 8088 }
+ }'
+```
+
+The response is synchronous and includes `success`, `containerId`, `url`, `message`, and `setupInstructions`. There is no separate status-poll endpoint; the dashboard updates live. See the [API docs](/docs/api/) for the full surface and the [Vaultwarden entry](/docs/catalog/vaultwarden/) for its volume path and environment variables.
+
+There is also an AI path: `POST /api/v1/ai/intent` returns a structured intent from plain English but does not deploy; your client or the built-in MCP server then calls `/api/v1/apps/deploy`.
+
+## Verify it actually works
+
+**Open the URL and check the padlock.** Browse to `https://vault.`. A real domain gets a trusted Let's Encrypt certificate automatically; a custom TLD is trusted once the DashCA root certificate is installed on the device. A browser warning means the app runs but trust is not set up yet.
+
+**Restart the container and confirm data persists.** Vaultwarden stores its data in the host volume `/opt/vaultwarden/data:/data`. Restart the container from the dashboard (or `docker restart `) and confirm your account is still there. Containers are disposable; volumes are not, and this proves your data is on disk.
+
+DashCaddy keeps watching: a health check runs every 30 seconds, and three consecutive failures mark the service **Unhealthy** on the dashboard.
+
+## Five things to do next
+
+1. **Deploy a second app.** Try [Jellyfin](/docs/catalog/jellyfin/) for media, [Immich](/docs/catalog/immich/) for photos, or [Uptime Kuma](/docs/catalog/uptime-kuma/) to watch the first two. Each gets its own subdomain and certificate on the same host.
+2. **Point a real domain at it.** With DNS integration configured, DashCaddy creates records automatically and serves a trusted certificate. Behind CGNAT, a tunnel works instead; [reaching self-hosted apps remotely](/blog/access-self-hosted-apps-remotely/) covers the options.
+3. **Browse the catalog.** There are 77 one-click templates across 17 categories in the [app catalog](/docs/catalog/); the [catalog guide](/blog/self-hosted-app-catalog-guide/) explains how to choose.
+4. **Set up backups.** Your state lives in `/opt/dockerdata` and `/etc/dashcaddy`; snapshot both off the host. The dashboard's Backup button exports a single JSON file for a no-setup option.
+5. **Learn the API.** The platform exposes 400+ REST route handlers, so anything you can click, you can script. Start with the [API docs](/docs/api/).
+
+## When DashCaddy is not the right choice
+
+- **You want exactly one app and enjoy hand-editing compose files.** If you run a single container and like tuning your own Caddyfile, plain Docker Compose will do the job without a control plane.
+- **You need Kubernetes-style orchestration.** DashCaddy runs Docker and Compose on a Linux host. A scheduler with Kubernetes semantics across a large container fleet is a different tool.
+- **You want deep per-container inspection in one narrow workflow.** A focused container tool may fit better than a full platform.
+
+## FAQ
+
+### Do I need a domain to start?
+
+No. A real domain gives you public DNS and a trusted Let's Encrypt certificate, but you can begin with a custom TLD on your LAN or a direct IP address and add a domain later. Nothing in the deployment flow changes.
+
+### How long does deploying an app take?
+
+For a template like Vaultwarden, the deploy waits up to 30 seconds for the health check to pass. Most of that time is pulling the image and starting the container.
+
+### What happens to my data if a container is removed?
+
+As long as your data lives in a mounted volume, it survives restarts and redeploys. Vaultwarden keeps everything in `/opt/vaultwarden/data` on the host, so removing the container alone does not delete it. That is also why backing up those volumes matters.
+
+If a step above did not behave as described, the [troubleshooting guide](/docs/troubleshooting/) walks the stack layer by layer, and the [product overview](/docs/overview/) explains how the pieces fit together. When you are ready to compare plans, see [pricing](/pricing/).
\ No newline at end of file
diff --git a/src/content/blog/manifest.ts b/src/content/blog/manifest.ts
index 7119b64..ac2519d 100644
--- a/src/content/blog/manifest.ts
+++ b/src/content/blog/manifest.ts
@@ -14,6 +14,36 @@ export interface PostMeta {
}
export const POSTS: PostMeta[] = [
+ {
+ slug: "getting-started-with-dashcaddy",
+ title: "Getting Started with DashCaddy: From Blank Server to a Running App",
+ description:
+ "A step-by-step walkthrough: pick a server, install DashCaddy with one command, open the dashboard, deploy your first self-hosted app, and get it on HTTPS.",
+ date: "2026-10-07",
+ author: "DashCaddy Team",
+ tags: ["dashcaddy getting started", "self-host apps", "docker deployment", "beginner homelab", "self-hosting tutorial"],
+ readingMinutes: 7,
+ },
+ {
+ slug: "self-host-vaultwarden",
+ title: "Self-Host Vaultwarden in 10 Minutes (Your Own Password Manager)",
+ description:
+ "Run your own Bitwarden-compatible password manager on your own server: install Vaultwarden with one click, put it on HTTPS, and keep your vaults off someone else's cloud.",
+ date: "2026-10-07",
+ author: "DashCaddy Team",
+ tags: ["self-host vaultwarden", "self hosted password manager", "bitwarden alternative", "vaultwarden docker", "homelab security"],
+ readingMinutes: 6,
+ },
+ {
+ slug: "self-host-your-photo-library",
+ title: "Self-Host Your Photo Library: Immich in 10 Minutes",
+ description:
+ "Stop paying a monthly subscription to look at your own photos. Deploy Immich with one click, put it behind HTTPS, and get automatic phone backup onto hardware you own.",
+ date: "2026-10-07",
+ author: "DashCaddy Team",
+ tags: ["self-host immich", "self hosted photo backup", "google photos alternative", "immich docker", "photo library"],
+ readingMinutes: 6,
+ },
{
slug: "self-hosted-app-catalog-guide",
title: "77 Self-Hosted Apps You Can Install in One Click (Full Catalog)",
diff --git a/src/content/blog/self-host-vaultwarden.md b/src/content/blog/self-host-vaultwarden.md
new file mode 100644
index 0000000..7889d82
--- /dev/null
+++ b/src/content/blog/self-host-vaultwarden.md
@@ -0,0 +1,98 @@
+---
+title: "Self-Host Vaultwarden in 10 Minutes (Your Own Password Manager)"
+description: "Run your own Bitwarden-compatible password manager on your own server: install Vaultwarden with one click, put it on HTTPS, and keep your vaults off someone else's cloud."
+date: "2026-10-07"
+author: "DashCaddy Team"
+tags: ["self-host vaultwarden", "self hosted password manager", "bitwarden alternative", "vaultwarden docker", "homelab security"]
+readingMinutes: 6
+---
+
+If you are deciding between Bitwarden and Vaultwarden, you are asking the wrong question. They are not competing products. Bitwarden is the company that runs a cloud service and publishes the open-source client apps. Vaultwarden is a lightweight, unofficial server that speaks the same protocol, so the official Bitwarden apps and browser extensions can talk to it. The real question is simpler: do you want your password database sitting on someone else's server, or on hardware you control? That is the whole decision. Vaultwarden just happens to be the easiest way to answer it.
+
+## What Vaultwarden is
+
+Vaultwarden is an unofficial, Bitwarden-compatible password manager server written in Rust. It implements the Bitwarden API closely enough that the official Bitwarden clients work against it unchanged. Because it is built for small deployments rather than millions of accounts, it stays small: one container, a data directory, and very little memory. DashCaddy ships it as a one-click template in the [app catalog](/docs/catalog/), which means the container, the reverse proxy, the DNS record, and the certificate are handled for you.
+
+## Why this is the best thing to self-host first
+
+If you self-host one service, make it the password manager. Every other service you deploy will need credentials: an admin account, an API key, a database password. If that pile of secrets lives in a consumer cloud vault, you have moved the most valuable data you own to a third party and accepted their uptime, their breach history, and their business model. A self-hosted vault flips that. Your encrypted database stays on your disk, and the only thing crossing the network is ciphertext the server cannot read. The [case for self-hosting](/blog/why-self-host-benefits/) is strongest here.
+
+## What "10 minutes" actually covers
+
+Ten minutes is honest if you have the prerequisites ready. It covers deploying the container, getting a valid TLS certificate, creating your account, and pointing the Bitwarden apps at your server. It does not cover buying a domain, waiting for DNS to propagate, or setting up backups, which is why those are worth doing before you start.
+
+## Prerequisites
+
+- **A domain.** This is not optional. Bitwarden's browser extensions and mobile apps refuse to talk to a plain `http://` origin; they need a real TLS origin. So you need a hostname like `vault.example.com` that resolves to your server.
+- **A server** running Linux with Docker, with ports 80 and 443 reachable from the internet.
+- **The decision to keep working backups.** A password vault on one disk is one disk failure away from losing every credential you own. Decide now where the second copy goes.
+
+Then install DashCaddy on that host:
+
+```bash
+curl -fsSL https://get.dashcaddy.net | bash
+```
+
+The [installation guide](/docs/installation/) covers the details.
+
+## Deploy Vaultwarden
+
+From the dashboard:
+
+1. Sign in and open the App Selector.
+2. Pick **Vaultwarden** from the Security category.
+3. Accept the suggested subdomain (`vault`) and port (`8088`), or change them.
+4. Click **Deploy**.
+
+DashCaddy pulls the `vaultwarden/server:latest` image, creates the persistent volume at `/opt/vaultwarden/data`, writes the Caddy reverse-proxy route, creates the DNS record, and requests the certificate. It waits for the container's health check to pass before it reports the service as **Running**. When it does, the URL is clickable.
+
+If you would rather script it, the same deploy is one API call with an `X-API-Key` header:
+
+```bash
+curl -X POST https://your-dashboard/api/v1/apps/deploy \
+ -H "X-API-Key: $DASHCADDY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"appId":"vaultwarden","config":{"subdomain":"vault","port":8088}}'
+```
+
+The response is synchronous and returns `success`, `containerId`, `url`, `message`, and `setupInstructions`. There is no polling step.
+
+## Make sure it is on HTTPS
+
+If the DNS record resolved and ports 80/443 were reachable, the certificate was issued during deploy and you are already done. If DashCaddy fell back to direct-IP access or a self-signed certificate, the usual cause is DNS not pointing at the host yet, or a firewall blocking the certificate challenge. The [troubleshooting guide](/docs/troubleshooting/) walks through it layer by layer. Verify the certificate is trusted before you create your account, because the apps will refuse a bad origin anyway.
+
+## Create your account, then close the door
+
+The default Vaultwarden template ships a `DOMAIN` and an `ADMIN_TOKEN`, and the first thing to do is change the admin token. Then create your own account, and once it exists, disable further signups so nobody else can register on your instance. This is the single most common mistake with a fresh Vaultwarden: leaving public registration open on a server that is now findable. The [Vaultwarden install guide](/docs/catalog/vaultwarden/) lists this in its post-install checklist.
+
+## Connect the real Bitwarden apps
+
+You do not need any Vaultwarden-specific client. Install the official Bitwarden browser extension, desktop app, or iOS/Android app, and before logging in, open the settings and set the self-hosted environment server URL to your own `https://vault.example.com`. Log in with the account you just created. Your vault now syncs to your hardware. If you want to reach it from outside your home network safely, read [accessing self-hosted apps remotely](/blog/access-self-hosted-apps-remotely/).
+
+## Backups are not optional here
+
+An encrypted vault on one disk is a single point of failure for every account you own. Export your vault periodically from the client, and back up the volume, which DashCaddy keeps at `/opt/vaultwarden/data`. Store the copy somewhere that is not the same machine. If you have not thought about this before, the [catalog guide](/blog/self-hosted-app-catalog-guide/) covers how backups fit into a self-hosted stack.
+
+## Honest caveats
+
+- **You are now the operator.** Uptime, upgrades, and backups are your job. DashCaddy manages the container lifecycle and the certificate, but it will not fix a broken restore for you.
+- **Vaultwarden is not the official Bitwarden server.** It is a compatible reimplementation, and it can occasionally lag new Bitwarden client features. For most personal and family use this does not matter; for an enterprise with compliance requirements, it might.
+- **Real secrets deserve real protection.** Turn on two-factor authentication on your account, use a strong master password you have not reused anywhere, and keep the admin token private.
+
+If self-hosting is new to you, start with [what DashCaddy is](/blog/what-is-dashcaddy/) and [getting started](/blog/getting-started-with-dashcaddy/), then come back. The software itself is free; what you pay for, if anything, is DashCaddy's premium features, which are described on the [pricing page](/pricing/).
+
+## FAQ
+
+### Is Vaultwarden the same as Bitwarden?
+
+No. Bitwarden is the official product and cloud service. Vaultwarden is an unofficial, Bitwarden-compatible server you run yourself. The official Bitwarden client apps connect to it because it implements the same API.
+
+### Does the official Bitwarden app work with my own server?
+
+Yes. In the app's settings, before you log in, select the self-hosted environment option and enter your own server URL. Everything else works the way it does against Bitwarden's cloud.
+
+### What happens if my server goes down?
+
+Your vault stops syncing, but the copies already downloaded to your phone, browser, and desktop remain readable offline. That is exactly why you keep a second backup copy somewhere else.
+
+You can run every one of these steps from one dashboard. Browse the [catalog](/docs/catalog/) to see what else deploys the same way, or add [Uptime Kuma](/docs/catalog/uptime-kuma/) to get alerted if the vault ever goes offline.
\ No newline at end of file
diff --git a/src/content/blog/self-host-your-photo-library.md b/src/content/blog/self-host-your-photo-library.md
new file mode 100644
index 0000000..f6aba0c
--- /dev/null
+++ b/src/content/blog/self-host-your-photo-library.md
@@ -0,0 +1,94 @@
+---
+title: "Self-Host Your Photo Library: Immich in 10 Minutes"
+description: "Stop paying a monthly subscription to look at your own photos. Deploy Immich with one click, put it behind HTTPS, and get automatic phone backup onto hardware you own."
+date: "2026-10-07"
+author: "DashCaddy Team"
+tags: ["self-host immich", "self hosted photo backup", "google photos alternative", "immich docker", "photo library"]
+readingMinutes: 6
+---
+
+Your photos are the one cloud subscription you will pay for forever, and the one library that only grows. Every year you add more, the backup gets bigger, and the cost that felt trivial at the start quietly compounds. Moving that library onto a machine you own ends the meter. Immich is the self-hosted photo and video app that makes it practical, and with DashCaddy it deploys with one click.
+
+## What Immich is
+
+Immich is a self-hosted photo and video library: automatic backup from your phone camera roll, a searchable timeline, albums, and sharing, with features such as object and face recognition running on machine learning. Its own project describes it as a Google Photos alternative, and DashCaddy lists it in the Photos category of the [app catalog](/docs/catalog/). It is not a thin file browser. It indexes your library, generates thumbnails, and gives you the timeline and albums you expect from a hosted service.
+
+## Why photos are the hardest thing to leave a cloud for
+
+Passwords can be exported in a file you carry around. Photos cannot. The library grows monotonically, it usually runs to tens or hundreds of gigabytes, and it is tied to a mobile app that has spent years syncing in the background without you thinking about it. Leaving means re-uploading everything, retraining your family's habits, and trusting that the new place will not lose a decade of images. That friction is the whole reason the subscription model works. Self-hosting removes the friction, but it also moves the responsibility to you, which is why the backup section below matters more than the install section.
+
+## Doing the storage maths honestly
+
+The number that matters is the free space you need at the destination, not the size of the upload. Two rules of thumb:
+
+- Estimate the library size by multiplying your photo count by an average file size. Phone photos commonly land in the 3 to 5 MB range; assume the higher end so you are not surprised.
+- Then plan for **at least double that figure in free space**, plus a second copy elsewhere.
+
+The doubling is not padding. Immich keeps the original as you uploaded it plus generated derivatives such as thumbnails and transcoded video, and any import or migration temporarily holds both source and destination. Running a photo server on a disk that is already 90 percent full is how people corrupt libraries. If your library is 200 GB, do not point Immich at a 250 GB disk. Point it at something with room to grow, because the library will keep growing.
+
+## Prerequisites
+
+- **A domain** for HTTPS, because the mobile app wants a real TLS origin and you will be reaching this from your phone over cellular. A hostname like `photos.example.com` resolving to your server is enough.
+- **A Linux host with Docker**, with ports 80 and 443 reachable.
+- **Storage with headroom**, per the maths above.
+- **A decision about the second copy.** Decide where the off-machine backup goes before you upload anything you cannot replace.
+
+Install DashCaddy on that host:
+
+```bash
+curl -fsSL https://get.dashcaddy.net | bash
+```
+
+## Deploy Immich
+
+From the dashboard:
+
+1. Sign in and open the App Selector.
+2. Pick **Immich** from the Photos category.
+3. Accept the suggested subdomain (`photos`) and port (`2283`), or change them.
+4. Click **Deploy**.
+
+DashCaddy pulls the `ghcr.io/immich-app/immich-server:latest` image, provisions the database and cache dependencies Immich needs, creates the persistent volumes at `/opt/immich/upload` and `/opt/immich/library`, writes the Caddy route, creates the DNS record, and requests the certificate. It waits for the container's health check at `/api/server-info/ping` before it reports the service as **Running**.
+
+Via the API, it is the same single call:
+
+```bash
+curl -X POST https://your-dashboard/api/v1/apps/deploy \
+ -H "X-API-Key: $DASHCADDY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"appId":"immich","config":{"subdomain":"photos","port":2283}}'
+```
+
+The response comes back synchronously with `success`, `containerId`, `url`, `message`, and `setupInstructions`.
+
+## Point your phone at your own server
+
+Create your admin account in the web UI, then install the Immich mobile app on iOS or Android. On the login screen, set the server URL to your own `https://photos.example.com` instead of the hosted default. Log in and enable automatic backup for the camera roll. From then on, new photos upload over your network to your hardware, and the first sync will pull up the existing back-catalogue. If you want to reach the server outside your home network without opening it to the world, the [remote access guide](/blog/access-self-hosted-apps-remotely/) covers the options.
+
+## The warning that matters
+
+Immich is under very active development, and this is the one thing its own documentation is loudest about. The project warns users to keep their own backups and to read the release notes before every upgrade, because breaking changes do happen between versions. Take that seriously. DashCaddy does not auto-update your containers, so upgrading is a deliberate action: pull the new image and restart the container yourself. Read the release notes before you do, and confirm your backup is current first. The [Immich install guide](/docs/catalog/immich/) repeats the same caution, and the [troubleshooting guide](/docs/troubleshooting/) covers what to do if an upgrade goes wrong.
+
+## Hardware notes, kept short
+
+Immich will run on a modest machine. Machine learning features such as face recognition are the hungry part: they want more RAM and CPU, and they are much faster on a GPU. If your hardware is small, you can run Immich without those features and still get the timeline, albums, and automatic backup, which is the core of the product. Start there and add horsepower only if you find you miss the smart features.
+
+## What about the photos already in your current cloud
+
+Do not delete anything until the new library is populated and verified. Before you start, use your current provider's bulk-download tool to export your entire library, then upload it to your Immich instance, either through the web UI in batches or by placing files directly in the library path. Confirm the photos and their dates are showing in the timeline, and only then cancel the subscription. Keep the old account read-only for a while if you can, as cheap insurance.
+
+## FAQ
+
+### Do I have to stop using my current photo app?
+
+No, but you should not run two sources of truth forever. The clean path is to export your existing library, import it into Immich, verify it, and then stop adding to the old one.
+
+### How much storage do I need?
+
+Roughly your library size times two or more, in free space, plus a separate copy on another machine or drive. Plan for it to keep growing.
+
+### Will Immich lose my photos on an update?
+
+Not if you follow the project's advice: keep your own backups and read the release notes before each upgrade. That is the honest answer, and it is exactly why the backup step comes before the install step.
+
+Immich is one of 77 one-click templates in the [catalog](/docs/catalog/), and it deploys the same way you have read here. The software is free and open source; if you want the premium platform features, they are described on the [pricing page](/pricing/). Start with [what DashCaddy is](/blog/what-is-dashcaddy/) if you are new, or see the wider [self-hosted app catalog guide](/blog/self-hosted-app-catalog-guide/) for what to run next.
\ No newline at end of file