# Blog Blog platform: a public website built with Nuxt, and a private Strapi CMS where the articles are written. Everything runs behind Caddy via Docker Compose. ```text Browser → Caddy ─┬─ public domain → Nuxt (website) └─ cms subdomain → Strapi (CMS) → PostgreSQL ``` There are no front-end accounts: sign-up is disabled and only administrators write 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. Generate the secrets** and paste them into `.env`. Strapi refuses to start with the placeholder values. ```bash openssl rand -base64 32 # run once per secret ``` Fill in `POSTGRES_PASSWORD`, both values of `APP_KEYS`, `API_TOKEN_SALT`, `ADMIN_JWT_SECRET`, `TRANSFER_TOKEN_SALT`, `JWT_SECRET` and `ENCRYPTION_KEY`. The domain and URL variables can stay as they are for local use. **3. Start the stack** ```bash docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build database cms 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. Create the administrator account** at http://localhost:1337/admin. This is the first run, so the form creates the account — pick your own credentials. **5. Write something.** In the admin panel: create a **Category**, then an **Article** (the body field is Markdown), then press **Publish** — the website only shows published content. **6. Open the website** at http://localhost:3000 — home, `/blog`, `/blog/` and `/category/`. Useful commands: ```bash docker compose logs -f cms # 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 database and media ``` To iterate on the code without rebuilding an image every time, run a package directly — `cd frontend && npm run dev`, or `cd cms && npm run develop`. The frontend defaults to `http://localhost:1337` for Strapi, so it works against the containerised CMS as is; override it with `NUXT_STRAPI_URL` if needed. Strapi reads its own `cms/.env`. ## Production **1. Point the DNS at the server.** Two `A` records (and `AAAA` if you have IPv6) on the public IP of the machine: | Record | Purpose | |---|---| | `example.com` | the website | | `cms.example.com` | the Strapi admin panel | Wait for the records to resolve before starting the stack — Caddy requests the certificates 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. PostgreSQL, Strapi and Nuxt are only reachable inside the Docker network — do not publish their ports. **3. Configure the environment.** Copy `.env.example` to `.env` on the server and set: | Variable | Value | |---|---| | `PUBLIC_DOMAIN` | `example.com` | | `CMS_DOMAIN` | `cms.example.com` | | `ACME_EMAIL` | a mailbox you read — Let's Encrypt sends expiry warnings there | | `PUBLIC_SITE_URL` | `https://example.com` | | `PUBLIC_STRAPI_URL` | `https://cms.example.com` | | `STRAPI_URL` | leave it as `http://cms:1337` — internal address, never public | Then generate **fresh** secrets for that machine (`openssl rand -base64 32`), different from the development ones. Keep `.env` out of version control; it is already ignored. > Changing `APP_KEYS`, `ADMIN_JWT_SECRET` or `JWT_SECRET` later logs everyone out. > Changing `ENCRYPTION_KEY` after content exists makes already-encrypted values > unreadable. Set them once, then back up the file somewhere safe. **4. Start everything** ```bash docker compose up -d --build ``` This time Caddy is included: it serves the website on `PUBLIC_DOMAIN`, the CMS on `CMS_DOMAIN`, obtains and renews the TLS certificates on its own, and adds HSTS and the other security headers. **5. Create the administrator account** at `https://cms.example.com/admin`, immediately, before anyone else finds the URL — the first visitor to that form is the one who gets the account. Then publish as in development. **6. Back up what is not in git**: the `postgres-data` volume (all content) and the `cms-uploads` volume (all images). Nothing else on the server holds state. ```bash docker compose exec -T database pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backup.sql docker run --rm -v blog_cms-uploads:/data -v "$PWD:/out" alpine tar czf /out/uploads.tar.gz -C /data . ``` **Updating a running site**: pull the new code, then `docker compose up -d --build`. Strapi applies its own schema changes 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).