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

99 lines
6.7 KiB
Markdown

# 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
8. `task docker:build` succeeds from the clean checkout, with no pre-built SPA.
9. `docker run --rm <image> --version` prints the version, which is not `dev`.
10. `docker image ls` size recorded. Flag it if it is above roughly 40 MB.
11. `docker image inspect` shows a non-root User, and `docker run --rm --entrypoint id <image>` shows uid not 0.
12. `docker history --no-trunc <image>` contains none of the values in docker.env.
## Run
13. `task docker:up` starts the service. `docker compose ps` shows it healthy within the healthcheck start period.
14. `curl -i http://localhost:8080/healthz` returns 200 and body `ok`.
15. `curl -i http://localhost:8080/` returns 200 text/html, and the SPA loads in a browser.
16. `curl -i http://localhost:8080/v1/auth/registration` returns 200 with JSON.
17. Logs show the listening line, and no permission error creating DB_FILE or UPLOAD_DIR on the volume.
18. 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
19. `docker compose down` (no -v), then `task docker:up`. The account from step 18 can log in and the image is still served.
20. `docker compose down -v` removes the volume, and the next start gives an empty instance. Documented in docs/docker.md.
21. The backup procedure in docs/docker.md is followed once and restores into a fresh volume.
## Failure and shutdown
22. 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`.
23. `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.
24. 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)
25. With an external SMTP host, trigger a registration or password-reset email and confirm delivery.
26. With a host MTA via host.docker.internal, repeat. On Linux this needs the extra_hosts mapping documented.
27. Confirm the failure mode is documented when SMTP is unreachable.
## Reverse proxy and TLS (manual)
28. Put Caddy or nginx in front, per docs/docker.md, with ENVIRONMENT=production over HTTPS. Log in and confirm the Secure cookie is accepted.
29. Set TRUSTED_PROXIES to the proxy address as seen by the container and confirm the login rate limiter uses X-Real-IP.
## Upgrade
30. Follow docs/docker.md: back up, rebuild with `task docker:build`, `task docker:up`. Data is intact and `--version` shows the new version.
31. 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 |