feat(deploy): lean multi-stage Dockerfiles + a shared upgrade path
Summary
Optimize the Docker build for every docs-kit site and give existing sites a real upgrade path for their Dockerfile — previously a one-shot heredoc in the new-site template that no upgrade ever touched.
There are two Docker flavors, both improved:
- Dogfood (
docs/Dockerfile, repo-root context, gem at/app+ app at/app/docs) - Standalone site (templated by
docs_kit:install,WORKDIR /rails, whole-app context)
What changed
| Change | Detail |
|---|---|
New root .dockerignore (dogfood) | Repo-root build context drops from 76 MB → ~24 MB: excludes node_modules (39 MB), .git (8.5 MB), coverage (2.4 MB), codedb.snapshot, specs, tmp/logs. |
docs/Dockerfile tightened | Prune the bundler cache + git-in-gems after bundle install; drop node_modules + bun/asset caches after assets:precompile, so the final COPY /app never carries the 39 MB of JS deps. |
docs_kit:install templates Dockerfile + .dockerignore | .dockerignore is gem-owned (refreshed every run, like the og rake task); Dockerfile is site-owned (skip-if-exists, like the config initializer). |
| Version-stamped upgrade path | The templated Dockerfile carries a # docs-kit Dockerfile vX.Y.Z marker. SyncReport warns on --sync when a site's marker is older than the gem's VERSION, pointing at the template to diff/replace. A hand-written Dockerfile (no marker) is left alone. |
new_site.rb deduplicated | Stops hand-writing the inline Dockerfile heredoc; defers to the generator so scaffolded and upgrading sites share ONE optimized file. A custom --service still corrects the LABEL. |
| Thruster fronts Puma | CMD ["./bin/thrust", "./bin/rails", "server"] — HTTP caching, compression, X-Sendfile. HTTP_PORT=3000 (the routed port: Kamal's app_port, EXPOSE, healthcheck), TARGET_PORT=3001 (Puma, unpublished). The template emits the thrust CMD only when the site bundles thruster in the production bundle (bin/thrust, or a Gemfile entry outside the development/test groups BUNDLE_WITHOUT excludes); otherwise it falls back to plain rails server, so docs_kit:install can never scaffold a CMD that crashes at boot. When the Gemfile has the gem but the binstub is missing, the generator scaffolds bin/thrust — the exec-form CMD needs the file to exist in the image. |
| Docs | README "Upgrading your Dockerfile" + a deploy-page "The Docker image" section. |
Why the explicit ports
Thruster's default HTTP_PORT is 80: as USER 1000 that bind can fail, and — worse — kamal-proxy routes to app_port: 3000, which would hit Puma directly and silently bypass Thruster. Pinning HTTP_PORT=3000/TARGET_PORT=3001 keeps Thruster on the routed port everywhere (Kamal, local docker run, the healthcheck).
Why drop .git is safe
The path-gem gemspec (docs-kit.gemspec) falls back to a Dir[] glob over exe/ lib/ app/ config/ when .git is absent. Verified: the glob yields the full 93-file manifest, so bundle install for the path gem still sees every file it needs.
Test Coverage
Generator spec (spec/generators/install_generator_spec.rb):
- writes
Dockerfile+.dockerignore - Dockerfile carries the
v<VERSION>marker - standalone
WORKDIR /railslayout, not the/app/docsmonorepo layout - multi-stage build keeps the toolchain out of the final image
.dockerignoreexcludesnode_modules,.git,log,tmp,spec,coverage- Dockerfile skip-if-exists (site-owned, never clobbered)
.dockerignoreforce-refresh (gem-owned)
SyncReport spec (drift detection):
- older marker → warns; equal marker → silent; no marker → silent
Verification
-
bundle exec rspec— 710/710 pass (thespec/rubocop/cop/**specs fail identically on cleanmaindue to a localrubocop-rakeresolution quirk;mainCI is green) -
bundle exec rubocop— no offenses -
docker build -f docs/Dockerfile .— succeeds, 327 MB image; boots and serves/up+/with HTTP 200; nonode_modules/.gitleaked; built CSS present - Thruster proven in the request path: Puma logs
Listening on :3001(TARGET_PORT honored), traffic answers on 3000, and the CSS asset returnsContent-Encoding: gzipwithAccept-Encoding: gzip— Puma alone never compresses, so the gzip response is Thruster serving - gemspec
Dir[]fallback verified for the.git-less build
Test plan for a reviewer
# The optimized dogfood image builds + runs from the repo root:
docker build -f docs/Dockerfile -t docs-kit:test .
docker run --rm -p 3000:3000 -e SECRET_KEY_BASE=test docs-kit:test
curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/up # → 200
# Thruster (not Puma) is answering — Puma alone never gzips:
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' \
"localhost:3000$(curl -s localhost:3000 | grep -oE '/assets/[^\"]+\.css' | head -1)" \
| grep -i content-encoding # → gzip
# The generator ships the Docker files + the version marker + the thrust CMD:
bundle exec rspec spec/generators/install_generator_spec.rb