Files
RenewCMS/features/docker-deployment.qa.md

6.7 KiB

QA: Docker deployment

Packaging and docs only. There is no Go test in test/ and no Vitest spec for this slice. All steps are manual command steps run from a clean checkout on a machine with Docker and Compose v2. Record results in the Notes table. Anything not run is reported NOT RUN, never PASS.

Preconditions

  • Clean checkout, no bin/, tmp/, node_modules or internal/infrastructure/assets/dist present.
  • docker.env created from docker.env.example. JWT_SECRET is generated (openssl rand -hex 32) and SMTP values are real or placeholders. Nothing is committed.
  • For local HTTP trial, ENVIRONMENT=development (otherwise the Secure cookie is not stored over HTTP).

Static checks (no Docker daemon needed)

  1. Dockerfile, .dockerignore and docker-compose.yml contain no comment lines (no line starting with #, no trailing #).
  2. .dockerignore lists tmp/, bin/, node_modules (also web/node_modules), .git, internal/infrastructure/assets/dist, .env and docker.env.
  3. Dockerfile: three stages (bun, go, final). The Go build sets CGO_ENABLED=0, has no -tags nofrontend, and uses -ldflags containing -X main.version= from a build arg. The final stage has USER non-root, exec-form ENTRYPOINT (no sh -c), and no ENV, ARG or COPY holding JWT_SECRET or any SMTP_* value.
  4. docker-compose.yml: one service, a named volume mounted at the data directory, a ports entry, env_file, a healthcheck on /healthz, stop_grace_period >= 15s.
  5. git diff --stat shows no change under api/, internal/, web/src/, cmd/ and test/. Only Dockerfile, .dockerignore, docker-compose.yml, docker.env.example, Taskfile.yml, .gitignore, README.md and docs/ changed. No new env var appears in cmd/server/main.go.
  6. docs/docker.md exists and is linked from README.md and docs/deployment.md.
  7. task --list shows docker:build, docker:run, docker:up, docker:down and docker:logs.

Build

  1. task docker:build succeeds from the clean checkout, with no pre-built SPA.
  2. docker run --rm <image> --version prints the version, which is not dev.
  3. docker image ls size recorded. Flag it if it is above roughly 40 MB.
  4. docker image inspect shows a non-root User, and docker run --rm --entrypoint id <image> shows uid not 0.
  5. docker history --no-trunc <image> contains none of the values in docker.env.

Run

  1. task docker:up starts the service. docker compose ps shows it healthy within the healthcheck start period.
  2. curl -i http://localhost:8080/healthz returns 200 and body ok.
  3. curl -i http://localhost:8080/ returns 200 text/html, and the SPA loads in a browser.
  4. curl -i http://localhost:8080/v1/auth/registration returns 200 with JSON.
  5. Logs show the listening line, and no permission error creating DB_FILE or UPLOAD_DIR on the volume.
  6. Register an account in the SPA, then upload an image, to confirm both the DB and UPLOAD_DIR are writable by the container user.

Persistence

  1. docker compose down (no -v), then task docker:up. The account from step 18 can log in and the image is still served.
  2. docker compose down -v removes the volume, and the next start gives an empty instance. Documented in docs/docker.md.
  3. The backup procedure in docs/docker.md is followed once and restores into a fresh volume.

Failure and shutdown

  1. For each required variable in turn, remove it from the env file and run docker compose up. The container exits non-zero and the log contains Environment variable <NAME> is not set.
  2. docker compose stop (timed) completes in under 10 s, the log contains shutting down..., and the exit code is 0. This proves PID 1 receives SIGTERM.
  3. Break the healthcheck (stop the app inside the container, or point it at the wrong port): the service becomes unhealthy.

SMTP (manual, needs real SMTP)

  1. With an external SMTP host, trigger a registration or password-reset email and confirm delivery.
  2. With a host MTA via host.docker.internal, repeat. On Linux this needs the extra_hosts mapping documented.
  3. Confirm the failure mode is documented when SMTP is unreachable.

Reverse proxy and TLS (manual)

  1. Put Caddy or nginx in front, per docs/docker.md, with ENVIRONMENT=production over HTTPS. Log in and confirm the Secure cookie is accepted.
  2. Set TRUSTED_PROXIES to the proxy address as seen by the container and confirm the login rate limiter uses X-Real-IP.

Upgrade

  1. Follow docs/docker.md: back up, rebuild with task docker:build, task docker:up. Data is intact and --version shows the new version.
  2. Confirm docs state that browser-update Restart is unsupported in Docker (Settings shows "Not prepared").

Notes to record

Item Value
Image size
Base images and tags used
Container uid
Build time (cold)
Docker and Compose versions
Steps 25-29 run?

Not run in this slice: CI, registry publishing, Kubernetes or Helm, multi-arch. SPA tests and visual checks are out of scope.

Results

Run on Windows 11, Docker 29.8.1, Compose 5.5.1, with a throwaway docker.env (fake secrets, ENVIRONMENT=development) that was deleted afterwards.

Steps Result
1-7, 31 PASS
8 PASS after a fix: bun run build failed in the image because vue-tsc cannot resolve .vue modules under the Bun-only node fallback; the Dockerfile now runs bun run build-only. vue-tsc passes on the host and under real node 22
9, 11, 12 PASS (version v2.8.0-beta-dirty, uid 10001, entrypoint exec form, no secret names in history)
10 54.5 MB, above the 40 MB flag (binary 29.9 MB on alpine 3.22)
13-17 PASS (healthy, /healthz 200 ok, / 200 text/html, /v1/auth/registration 200 JSON, no permission errors)
18-19 PASS (register, verify, login, image upload to /data/uploads as uid 10001; all survive down and recreate; uploaded image still served)
20 PASS (down -v removed the volume)
21 PASS (tar backup, down -v, restore, login and media list intact)
22 PASS for all 7 vars (exit 2, Environment variable X is not set)
23 PASS (stop 0.4 s, exit 0, shutting down... logged)
24 PASS (wrong-port healthcheck reports unhealthy)
25 PARTIAL: mail delivered over STARTTLS to a Mailpit container on the compose network with the CA trusted via SSL_CERT_FILE; no real external host
26-29 NOT RUN (host MTA, Caddy/nginx TLS, TRUSTED_PROXIES)
30 NOT RUN as a before/after upgrade
task docker:run PASS (healthz 200, graceful stop, separate volume renewcms-data)

Notes

Item Value
Image size 54.5 MB
Base images and tags used oven/bun:1-alpine, golang:1.27.1-alpine, alpine:3.22
Container uid 10001
Build time (cold) not measured, layers were partly cached; about 18 s warm
Docker and Compose versions 29.8.1, 5.5.1
Steps 25-29 run? 25 partial, 26-29 no