# Blog Blog platform: a public website built with Nuxt, and a private PocketBase CMS where the articles are written. Everything runs behind Caddy via Docker Compose. ```text Browser → Caddy ─┬─ /_/*, /api/* → PocketBase (CMS) └─ everything else → Nuxt (website), which also redirects /admin → /_/ ``` There are no front-end accounts: only the superuser writes content. An article is a title, a Markdown body, a cover image and a category. ## Development No domain and no TLS needed — the services are published on localhost instead of going through Caddy. Requires Docker. **1. Create the environment file** ```bash cp .env.example .env ``` **2. Set the admin credentials.** Replace `POCKETBASE_ADMIN_EMAIL` and `POCKETBASE_ADMIN_PASSWORD` in `.env` with your own — this is the superuser account PocketBase creates (or updates) on every start. The domain and URL variables can stay as they are for local use. ```bash chmod 600 .env ``` **3. Start the stack** ```bash docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase frontend ``` The first build takes a few minutes. `docker-compose.dev.yml` publishes the ports on `127.0.0.1` and leaves Caddy out; it must always be passed explicitly, so it can never be picked up by accident in production. **4. Open the admin UI** at http://localhost:3000/admin (redirects to PocketBase's dashboard — this works in dev too, without Caddy, since the redirect is handled by the frontend itself) and log in with the credentials from `.env`. **5. Write something.** In the admin UI: create a **categories** record, then an **articles** record (the `content` field is Markdown), then set `publishedAt` — the website only shows articles whose `publishedAt` is set and not in the future. **6. Open the website** at http://localhost:3000 — home, `/blog`, `/blog/` and `/category/`. Useful commands: ```bash docker compose logs -f pocketbase # follow the CMS logs docker compose -f docker-compose.yml -f docker-compose.dev.yml restart frontend docker compose down # stop, keep the data docker compose down -v # stop and WIPE the CMS database and media ``` To iterate on the frontend without rebuilding an image every time: `cd frontend && npm run dev`. It defaults to `http://localhost:8090` for PocketBase, so it works against the containerised CMS as is; override it with `NUXT_POCKETBASE_URL` if needed. ## Production **1. Point the DNS at the server.** One `A` record (and `AAAA` if you have IPv6) on the public IP of the machine: | Record | Purpose | |---|---| | `example.com` | the website, and, at `/admin`, the PocketBase admin UI | Wait for the record to resolve before starting the stack — Caddy requests the certificate on the first boot and a failed challenge means a retry delay. **2. Open the firewall** for ports `80` and `443` only. Port `80` is required: Caddy uses it for the ACME challenge and to redirect to HTTPS. The admin UI shares port 443 with the site (reachable by anyone who knows `/admin`, protected only by the superuser login, so keep the password strong); PocketBase itself is never published directly, only Caddy's proxy to it. **3. Configure the environment.** Copy `.env.example` to `.env` on the server and set: | Variable | Value | |---|---| | `PUBLIC_DOMAIN` | `example.com` | | `ACME_EMAIL` | a mailbox you read — Let's Encrypt sends expiry warnings there | | `PUBLIC_SITE_URL` | `https://example.com` | | `PUBLIC_POCKETBASE_URL` | `https://example.com` — same origin, Caddy proxies `/_/` and `/api/` there | | `POCKETBASE_URL` | leave it as `http://pocketbase:8090` — internal address, never public | | `POCKETBASE_ADMIN_EMAIL` / `POCKETBASE_ADMIN_PASSWORD` | your real superuser credentials | Keep `.env` out of version control; it is already ignored. > Changing `POCKETBASE_ADMIN_PASSWORD` later and restarting rotates the superuser > password immediately (the entrypoint upserts it on every boot) — a credential change, > so treat it with the same care as any production credential rotation. **4. Start everything** ```bash docker compose up -d --build ``` This time Caddy is included: it serves both the website and, under `/admin`, the CMS admin UI on `PUBLIC_DOMAIN`, obtains and renews the TLS certificate on its own, and adds HSTS and the other security headers. **5. Log into the admin UI** at `https://example.com/admin` with the credentials from `.env`, then publish as in development. **6. Back up what is not in git**: the `pocketbase-data` volume (database and uploaded media together). Nothing else on the server holds state. ```bash docker run --rm -v blog_pocketbase-data:/data -v "$PWD:/out" alpine tar czf /out/pocketbase-data.tar.gz -C /data . ``` **Updating a running site**: pull the new code, then `docker compose up -d --build`. PocketBase applies new `pb_migrations/` files at startup; take a backup first. Architecture, conventions and constraints are documented in [CLAUDE.md](CLAUDE.md). ## License Proprietary — Copyright (c) 2026 Davide Grilli. All rights reserved. See [LICENSE](LICENSE).