The first deploy
The service file
Section titled “The service file”compose.jonathandobson.yml sits next to the starter’s compose.yml:
secrets: resend_api_key: file: ./secrets/resend_api_key.txt turnstile_secret_key: file: ./secrets/turnstile_secret_key.txt
services: jonathandobson: image: ${JONATHANDOBSON_IMAGE:?set JONATHANDOBSON_IMAGE in .env} container_name: jonathandobson restart: unless-stopped read_only: true tmpfs: - /app/.next/cache:uid=1000,gid=1000 secrets: [resend_api_key, turnstile_secret_key] environment: - RESEND_API_KEY_FILE=/run/secrets/resend_api_key - TURNSTILE_SECRET_KEY_FILE=/run/secrets/turnstile_secret_key - CONTACT_TO_EMAIL=<private inbox> - CONTACT_FROM_EMAIL=<sender on your Resend-verified domain> - CONFIRMATION_FROM_EMAIL=<no-reply sender on the same domain> - SITE_HOSTNAME=jonathandobson.com networks: [traefik_public] labels: - "traefik.enable=true" - "traefik.docker.network=traefik_public" - "traefik.http.routers.jonathandobson.rule=Host(`jonathandobson.com`) || Host(`www.jonathandobson.com`)" - "traefik.http.routers.jonathandobson.entrypoints=websecure" - "traefik.http.routers.jonathandobson.tls.certresolver=letsencrypt" - "traefik.http.services.jonathandobson.loadbalancer.server.url=http://jonathandobson:3000" - "traefik.http.routers.jonathandobson.middlewares=rate-limit@file,secure-headers@file"
networks: traefik_public: external: trueStart it — naming the files
Section titled “Start it — naming the files”The starter’s Makefile only adds its own monitoring file, so make up would
not include this service. Always name the files:
docker compose -f compose.yml -f compose.jonathandobson.yml up -d jonathandobsonProve it works
Section titled “Prove it works”On the server, through Traefik:
R="--resolve jonathandobson.com:443:127.0.0.1"for p in / /standard /holiday /nope /a/b /sitemap.xml /robots.txt /holiday/opengraph-image; do printf '%-26s %s\n' $p "$(curl -s -o /dev/null -w '%{http_code}' $R https://jonathandobson.com$p)"; donecurl -s $R https://jonathandobson.com/ | grep -o 'data-season="[a-z]*"'curl -sI $R https://jonathandobson.com/ | grep -i -E 'strict-transport|x-robots|x-frame|nosniff'Expected: 200s, 404 for /nope and /a/b, today’s season, the security
headers — and no X-Robots-Tag.
From outside, through Cloudflare: https:// and www. return 200, http://
redirects, and cf-cache-status: DYNAMIC — Cloudflare doesn’t cache HTML by
default, so the January 1 theme switch won’t lag.
Finally, send one real message through the form and confirm both emails arrive.
A harmless log line
Section titled “A harmless log line”Bots probe paths like /wp-json. They get a correct 404, but Next.js then tries
to cache each 404 page in .next/server/route-cache, which the read-only
container can’t write, and logs ENOENT. Visitors are unaffected. A small,
size-capped tmpfs at that path would quiet it.