Skip to content

chore(docker): slim image and drop runtime git - #5

Closed
Fabio1988 wants to merge 6 commits into
mainfrom
chore/docker-slim
Closed

Fabio1988 wants to merge 6 commits into
mainfrom
chore/docker-slim

Conversation

@Fabio1988

@Fabio1988 Fabio1988 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Slims the Docker image from 3 GB → 608 MB uncompressed (~1.4 GB → ~207 MB compressed) and removes the runtime dependency on git / .git.

:main today this PR
Uncompressed 3 GB 608 MB
Compressed (gzip) ~1.4 GB ~207 MB
Yarn download cache in image 2.2 GB 0
.git / git binary yes no

Where the size went

  • yarn install left the yarn download cache (/usr/local/share/.cache, 2.2 GB) in the layer.
  • devDependencies (eslint, typescript, semantic-release, monaco-editor, …) shipped in the runtime image.
  • No .dockerignore, so local docker builds copied the host's node_modules, dist and config files.

Changes

  • Dockerfile: three stages, build → prune → runtime.
    • BuildKit cache mount for the yarn cache.
    • git is only installed in the build stage, for the git-URL dependencies chalkercli and passport-discord.
    • The runtime stage is a fresh node:22.23.4-alpine with production node_modules. Paths stay under /home/node, so existing compose mounts keep working.
    • Default CMD ["yarn", "start"].
  • .dockerignore: new. It also keeps config/local.json, config/areas.json, server/src/configs/** and .env out of locally built images.
  • Git metadata: GIT_SHA / GIT_REF build args become env vars and replace the .gitsha / .gitref files.
  • checkForUpdates.js:
    • Reads the sha and ref from the env; bare-metal installs still use local git.
    • Gets the remote sha from the GitHub API (application/vnd.github.sha) instead of git ls-remote.
    • Skips non-branch refs. PR images used to parse refs/pull/123/merge as branch "123".
  • Dependencies: yarn start rebuilds the client on every boot, baking in the user's title, theme, start location, GA, Sentry and favicon. So the build toolchain is a real runtime dependency:
    • vite, @vitejs/plugin-react-swc, rollup-plugin-delete, @sentry/vite-plugin and @rm/vite-plugins move to dependencies.
    • vite-plugin-checker is lazy-loaded (dev only) and stays a devDependency.
    • openai and gpt-tokenizer (only used by the CI locales:generate job) move to @rm/locales devDependencies. The unused generate re-export is dropped, so the server no longer loads them at boot. index.js now calls dotenv.config() itself to keep the .env loading generate.js used to provide as a side effect; vite.config.js relies on it for NODE_CONFIG_ENV → dist-*.
    • The unused monaco-editor devDependency is removed. The playground keeps working: @monaco-editor/loader loads Monaco from cdn.jsdelivr.net/npm/monaco-editor@0.55.1, and the npm copy (^0.41) was never bundled. Install now prints an unmet-peer warning.
  • docker.yml:
    • Drops the .gitsha / .gitref steps and adds setup-buildx-action.
    • Bumps to login v3, metadata v5, build-push v6.
    • Passes the build args and uses the GHA layer cache (type=gha,mode=max).
    • Adds explicit permissions (contents: read, packages: write).

Parity fixes (side effects of the old image that were kept)

  • @rm/locales still loads .env on import. generate.js used to do this as a side effect, and vite.config.js relies on it for NODE_CONFIG_ENV → dist-*.
  • The Sentry vite plugin release name falls back to SENTRY_RELEASE → GIT_SHA. It used to auto-detect via git rev-parse HEAD, which only worked because the old image shipped git and .git.
  • NPM_CONFIG_PREFIX / PATH are kept for derived images.
  • provenance: false, so ghcr still receives a single image manifest. Without it, buildx would publish an attestation index with an extra unknown/unknown entry.

Intentional behavior changes

  • Update check, bare-metal: same as before (local git branch / git rev-parse). The tracked empty .gitref/.gitsha placeholders made the old code fall through to git correctly. Only the remote lookup changed (see below). A branch that doesn't exist upstream now logs Unable to get remote SHA instead of a false "new version available" with an empty sha.
  • Update check, Docker: branches containing / are no longer truncated (feature/x used to become feature). PR images skip the check.
  • Update check, failures: no false "new version" notice when the remote sha can't be fetched. The remote lookup has a 10s timeout; git ls-remote had none, and startup awaits it.
  • Update check, API: the remote sha comes from api.github.com (unauthenticated limit: 60 requests/hour per IP, and the check runs once at startup) instead of git ls-remote to github.com.
  • Image contents: devDependencies are no longer in the image, so yarn dev, yarn lint, yarn locales:generate, yarn test etc. don't work inside the container. yarn start, yarn server, yarn build, yarn masterfile and the migrate:* scripts do.
  • Local docker build without build args: the update check is skipped quietly. Old local builds had git + .git, but git refused with dubious ownership in repository at '/home/node' and printed a false "new version available". Pass --build-arg GIT_SHA=$(git rev-parse HEAD) --build-arg GIT_REF=$(git symbolic-ref HEAD) to enable the check.
  • Manifest media type: buildx pushes an OCI image manifest (application/vnd.oci.image.manifest.v1+json) instead of Docker v2. It is still a single manifest (no index, provenance: false), and the docker, podman and watchtower versions of the last several years support it.
  • Default command: the image defaults to CMD ["yarn", "start"] (previously the base node REPL). Compose setups that set command are unaffected.
  • yarn install warning: it now prints an unmet-peer warning for monaco-editor.

Old vs new comparison

The same mounted local.json (custom headerTitle, MariaDB) was used in every setup. Probes: /, /playground, /index.html, /favicon.ico, /locales/{en,de}/translation.json, a 404, /api/v1/config, /auth/discord, POST /graphql, the config baked into the bundle, and sha256 of every dist file.

Setup Old (main) New (this PR) Result
Local node (yarn start) main worktree branch HTTP responses byte-identical, all 56 dist files identical
Local docker build (no args, config mounted) 3.03 GB 608 MB HTTP byte-identical, dist identical, startup build bakes the config in both
CI image (yarn start) watwowmap/reactmap:main (v1.57.0) fabio1988/reactmap:pr-5 (v1.55.2) Same status codes and payloads apart from the expected version-specific index.html and bundle; CI-built new image is identical to the local new build

PR CI on the fork: lint ✓, docker build + push ✓ (cache, build args, single manifest).

Testing

  • yarn lint, yarn prettier, yarn config:check, yarn masterfile, yarn test and yarn build all pass locally. These are the same steps as lint.yml, config.yml and release.yml.
  • openai / gpt-tokenizer still resolve for yarn locales:generate, since locales.yml does a full install.
  • Built the image locally and ran it against a throwaway MariaDB:
    • The masterfile, vite build and locales are generated at startup; the server listens and there are no missing modules.
    • GET / and GET /playground return 200.
    • Update check on refs/heads/main reports the newer remote sha with the docker-compose pull hint.
    • Update check on refs/pull/12/merge skips cleanly.
    • Without build args there's no false notice.
  • Node in the image is v22.23.4, which includes the CVE-2026-111497 (Node2Shell) brotli fix.

🤖 Generated with Claude Code

Fabio1988 and others added 6 commits November 3, 2026 22:28
- Multi-stage Dockerfile (build -> prune -> runtime) with a BuildKit
  yarn cache mount, so the 2.2 GB yarn cache never lands in a layer
- Add .dockerignore (also keeps local configs/secrets out of images)
- Pass GIT_SHA/GIT_REF as build args instead of .gitsha/.gitref files
- checkForUpdates: read sha/ref from env, fetch remote sha via the
  GitHub API (no git binary needed), skip non-branch refs (PR images)
- Move vite + build plugins to dependencies since `yarn start` builds
  the client on boot; lazy-load vite-plugin-checker (dev only)
- Move openai/gpt-tokenizer to @rm/locales devDependencies and drop the
  unused `generate` re-export so the server no longer loads them
- Remove unused monaco-editor devDependency (playground loads Monaco
  from jsDelivr via @monaco-editor/loader)
- docker.yml: buildx, current action versions, GHA layer cache

Image: 3 GB -> 608 MB uncompressed (~1.4 GB -> ~207 MB compressed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the `generate` re-export also removed its `dotenv.config()`
side effect. vite.config.js relied on it (via ReactMap.js/`yarn build`)
to pick up NODE_CONFIG_ENV from `.env` for the dist-* output dir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vite.config: name the Sentry release from SENTRY_RELEASE/GIT_SHA;
  the plugin used to auto-detect it via `git rev-parse HEAD`, which
  worked in the old image because it shipped git and .git
- Dockerfile: keep NPM_CONFIG_PREFIX/PATH for derived images
- docker.yml: provenance: false so ghcr still gets a single manifest
  instead of an attestation index

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI images never contained them and the documented setup mounts configs
as volumes, so baking them into local builds is not something to keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Docker image now receives GIT_SHA/GIT_REF as build args and
checkForUpdates no longer reads these files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Fabio1988

Copy link
Copy Markdown
Owner Author

Superseded by #6, which tracks chore/docker-slim-v2 (the branch behind upstream WatWowMap#1279).

@Fabio1988 Fabio1988 closed this Nov 3, 2026
@Fabio1988
Fabio1988 deleted the chore/docker-slim branch November 3, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant