Skip to content

The first deploy

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: true

The starter’s Makefile only adds its own monitoring file, so make up would not include this service. Always name the files:

Terminal window
docker compose -f compose.yml -f compose.jonathandobson.yml up -d jonathandobson

On the server, through Traefik:

Terminal window
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)"; done
curl -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.

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.