Docker Container Builder

Generates production-ready, optimized Dockerfiles with multi-stage builds, caching, and tagging for any application.

// prompt
You are a senior DevOps engineer specializing in container image optimization. Build a production-ready Docker setup for the application **{{application_name}}**. ## Application Context - **Language / runtime:** {{language_and_version}} - **Build / dependency tool:** {{build_tool_eg_npm_pip_maven_go_modules}} - **Listening port(s):** {{exposed_port}} - **Runtime requirements:** {{special_requirements_eg_system_libs_env_vars_secrets}} - **Target registry & tag scheme:** {{registry_tag_convention}} ## What to Deliver 1. **Multi-stage Dockerfile** — separate build and runtime stages so the final image ships only what it needs to run. 2. **Optimized base images** — pick minimal, supported bases (slim/alpine/distroless where appropriate) and pin versions explicitly. Justify each choice in one line. 3. **Layer caching** — order instructions so dependency manifests are copied and installed before source code, maximizing cache reuse on rebuilds. 4. **Security hardening** — run as a non-root user, avoid leaking secrets into layers, and use `.dockerignore` to exclude build artifacts and local config. 5. **Runtime config** — set required `ENV` variables, `EXPOSE` the correct port, and define a clear `ENTRYPOINT`/`CMD`. Add a `HEALTHCHECK` if relevant. 6. **Tagging & build commands** — provide the `docker build` and `docker tag` commands using semantic version + name (e.g. `name:1.2.0`, `name:latest`). ## Output Format For each artifact, return: - A short rationale (2-3 sentences) explaining the design decisions. - The complete file in a fenced code block (`Dockerfile`, `.dockerignore`). - The exact build, tag, and run commands. - A brief note on the expected final image size and how it was reduced. Constraints: keep the final image as small as possible without sacrificing reproducibility, comment any non-obvious instruction inline, and flag any assumption you had to make about {{application_name}}. Ask me for missing details before generating if a required field above is unclear.
Fill in the variables

Example run

Generated by Claude Opus 5.5 on September 23, 2026

Inputs used

Application name
invoice-sync-api
Language and version
Node.js 20.11.1 LTS (using the official 20-bookworm toolchain)
Build tool, e.g. npm, pip, maven, go modules
npm 10 (package-lock.json committed, npm ci for reproducible installs)
Exposed port
3000 (HTTP API), plus 9229 only in dev for the Node inspector (not exposed in prod)
Special requirements, e.g. system libs, env vars, secrets
The app is an Express 4 REST service that syncs invoices between our billing DB and a third-party accounting API. It uses the `sharp` package for thumbnailing attached PDFs, which needs libvips (>=8.14) and its build headers (build-essential, python3) present at `npm install` time but NOT in the final runtime image — sharp ships prebuilt binaries for linux-x64 so only the shared libvips runtime lib is needed at runtime. Environment variables required at container start: - DATABASE_URL (Postgres 15 connection string, e.g. postgres://user:pass@db:5432/invoices) - REDIS_URL (used for job queue caching, e.g. redis://redis:6379/0) - JWT_SECRET (32+ char secret, injected via orchestrator secret store, never baked into the image) - STRIPE_API_KEY (injected the same way, used to reconcile invoice payments) - NODE_ENV=production - TZ=UTC (all invoice timestamps must be stored/logged in UTC) - LOG_LEVEL=info The service writes temporary PDF renders to /app/tmp, which must be writable by the non-root runtime user and ideally mounted as a tmpfs/volume in production. It exposes a liveness/readiness endpoint at GET /healthz returning 200 with JSON `{"status":"ok"}`. It's typically run behind an nginx reverse proxy, so `app.set('trust proxy', 1)` is already set in code — no extra flag needed in the container. No native compiler should remain in the final image; all devDependencies (jest, eslint, nodemon, supertest) must be excluded from the production node_modules.
Registry / tag convention
Push to GitHub Container Registry as ghcr.io/vongu3/invoice-sync-api. Tag with semver from package.json (e.g. invoice-sync-api:1.4.2), also push invoice-sync-api:1.4 and invoice-sync-api:1 rolling tags, invoice-sync-api:latest for the current main branch build, and invoice-sync-api:sha-<short-git-sha> for every CI build for traceability.

invoice-sync-api: Production Docker Setup

The spec was complete enough to build this without stopping to ask. One point in it does need your decision, though, so I made a default choice and flagged it.

⚠️ Read first: the sharp/libvips point

The spec says sharp's prebuilt linux-x64 binary needs a system libvips runtime library. That isn't how sharp's prebuilt binaries work. Since sharp 0.33, the prebuilt package includes its own libvips (via @img/sharp-libvips-linux-x64, about 17 MB). Earlier versions did the same through vendor/. A system libvips is only used if sharp is compiled from source against it.

So there are two consistent setups, and I exposed both through a build arg:

SHARP_LIBVIPS Builder installs Runtime installs Tradeoff
bundled (default) build-essential, python3 nothing extra Smallest image, uses sharp's tested libvips build
system + libvips-dev (Debian 12 ships 8.14.1, which meets your ≥8.14 requirement) libvips42 Uses your distro-patched libvips, but adds roughly 150–200 MB of shared libraries. Requires node-addon-api and node-gyp in your dependencies.

I defaulted to bundled because you asked for the smallest image. If you need system libvips (for example, for a compliance or patching policy), pass --build-arg SHARP_LIBVIPS=system.


1. Dockerfile

Rationale: There are four stages. The heavy node:20.11.1-bookworm toolchain installs production-only node_modules with compilers available. The runtime stage then copies just those modules and the source onto bookworm-slim, with no compiler, npm, yarn, or devDependencies. Optional dev and test targets share the same toolchain base, so local development (with the inspector on 9229) and CI tests never affect the production image. Application code and node_modules are owned by root and read-only to the app user; only /app/tmp is writable.

Base image choices:

  • node:20.11.1-bookworm (build stages): the official full toolchain, which already includes gcc, make, and python3, and uses the same Debian 12 glibc as the runtime. This keeps native binaries ABI-compatible.
  • node:20.11.1-bookworm-slim (runtime): the smallest official image that pins the exact Node patch version and uses glibc, which sharp's prebuilt binary requires.
    • Why not distroless: gcr.io/distroless/nodejs20 can't be pinned to 20.11.1 by tag, and it can't apt install libvips42 if you pick system mode.
    • Why not alpine: musl would need sharp's separate linuxmusl binaries and wouldn't match the glibc build stage.
# syntax=docker/dockerfile:1.7
# ^ Enables BuildKit features used below (RUN --mount cache mounts).

############################################################
# Global build args (must be declared before the first FROM
# to be usable in FROM lines; re-declared inside stages).
############################################################
ARG NODE_VERSION=20.11.1
ARG DEBIAN_RELEASE=bookworm
# bundled = use sharp's prebuilt binary with its own libvips (smallest)
# system  = compile sharp against Debian's libvips 8.14 (needs libvips42 at runtime)
ARG SHARP_LIBVIPS=bundled

############################################################
# Stage 1: build-base, full toolchain for native modules
############################################################
# For fully immutable builds, append @sha256:<digest> to both FROM lines
# (get it with: docker buildx imagetools inspect node:20.11.1-bookworm).
FROM node:${NODE_VERSION}-${DEBIAN_RELEASE} AS build-base
ARG SHARP_LIBVIPS
WORKDIR /app

# The apt cache mounts keep downloaded .debs out of the image and speed up rebuilds.
# The official image deletes the apt cache after every install via docker-clean;
# removing that hook lets the cache mount actually persist between builds.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y --no-install-recommends \
        build-essential \
        python3 \
        $( [ "$SHARP_LIBVIPS" = "system" ] && echo libvips-dev )

############################################################
# Stage 2: prod-deps, production-only node_modules
############################################################
FROM build-base AS prod-deps
ARG SHARP_LIBVIPS

# Copy ONLY the manifests first. This layer (and the expensive npm ci below)
# is reused from cache until package.json or package-lock.json changes.
COPY package.json package-lock.json ./

# --omit=dev        -> excludes jest, eslint, nodemon, supertest, etc.
# SHARP_* env vars  -> make sharp's install behavior explicit, not auto-detected
# The final require('sharp') fails the BUILD (not the deploy) if the native
# binding is broken.
RUN --mount=type=cache,target=/root/.npm \
    if [ "$SHARP_LIBVIPS" = "system" ]; then \
        export SHARP_FORCE_GLOBAL_LIBVIPS=1; \
    else \
        export SHARP_IGNORE_GLOBAL_LIBVIPS=1; \
    fi \
 && npm ci --omit=dev --no-audit --no-fund \
 && node -e "const s=require('sharp'); console.log('sharp OK, libvips', s.versions.vips)"

############################################################
# Stage 3a: dev, local development (NOT for production)
#   docker build --target dev -t invoice-sync-api:dev .
############################################################
FROM build-base AS dev
ENV NODE_ENV=development \
    TZ=UTC \
    LOG_LEVEL=debug
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund
COPY --chown=node:node . .
RUN mkdir -p /app/tmp && chown node:node /app/tmp
USER node
# 9229 = Node inspector. Exposed in dev ONLY.
EXPOSE 3000 9229
CMD ["npx", "nodemon", "--inspect=0.0.0.0:9229", "src/server.js"]

############################################################
# Stage 3b: test, CI gate (lint + unit tests)
#   docker build --target test .
############################################################
FROM dev AS test
ENV NODE_ENV=test
RUN npm run lint && npm test

############################################################
# Stage 4: runtime, the production image
############################################################
FROM node:${NODE_VERSION}-${DEBIAN_RELEASE}-slim AS runtime
ARG SHARP_LIBVIPS
ARG VERSION=0.0.0-dev
ARG GIT_SHA=unknown
ARG BUILD_DATE=unknown

# OCI labels: GHCR uses 'source' to link the package to the repo.
LABEL org.opencontainers.image.title="invoice-sync-api" \
      org.opencontainers.image.description="Syncs invoices between billing DB and accounting API" \
      org.opencontainers.image.source="https://github.com/vongu3/invoice-sync-api" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.revision="${GIT_SHA}" \
      org.opencontainers.image.created="${BUILD_DATE}"

# - tini: a tiny init (~30 KB) that runs as PID 1. It forwards SIGTERM so the
#   app can shut down gracefully on rolling deploys, and it reaps zombie processes.
# - libvips42: installed only in 'system' mode.
# - Removing npm/npx/corepack/yarn: they aren't needed at runtime, save ~20 MB,
#   and are a common target for CVE scanners.
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
        tini \
        $( [ "$SHARP_LIBVIPS" = "system" ] && echo libvips42 ) \
 && rm -rf /var/lib/apt/lists/* \
 && rm -rf /usr/local/lib/node_modules/npm \
           /usr/local/lib/node_modules/corepack \
           /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
           /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg

# Non-secret runtime defaults only. DATABASE_URL, REDIS_URL, JWT_SECRET, and
# STRIPE_API_KEY are deliberately NOT set here. They are injected at runtime by
# the orchestrator so they never appear in image layers or `docker history`.
# TZ=UTC needs no tzdata package: glibc and Node's ICU resolve UTC natively.
ENV NODE_ENV=production \
    TZ=UTC \
    LOG_LEVEL=info

WORKDIR /app

# Dependencies first (they change less often than source), then the app.
# Files stay root-owned, so the runtime user can read but not modify code or
# node_modules. This limits the damage from an RCE.
COPY --from=prod-deps /app/node_modules ./node_modules
COPY package.json ./
# Copy source explicitly (not `COPY . .`) so tests, configs, and docs can never
# leak into the production image, even if .dockerignore drifts.
COPY src/ ./src/

# The only writable path. Mount a tmpfs over it in production (see run command).
RUN mkdir -p /app/tmp && chown node:node /app/tmp && chmod 0700 /app/tmp

# Numeric UID:GID lets Kubernetes' runAsNonRoot verify non-root status without
# looking up /etc/passwd. UID 1000 is the built-in 'node' user.
USER 1000:1000

EXPOSE 3000

# There is no curl or wget in slim, so use Node 20's built-in fetch. The check
# requires both HTTP 200 and the {"status":"ok"} body.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD ["node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>r.json().then(b=>process.exit(r.ok&&b.status==='ok'?0:1))).catch(()=>process.exit(1))"]

ENTRYPOINT ["/usr/bin/tini", "--"]
# Run node directly, NOT `npm start`. npm doesn't forward signals and isn't
# installed in this stage anyway.
CMD ["node", "src/server.js"]

2. .dockerignore

Rationale: Keeping the build context small speeds up every build and prevents local node_modules (possibly built for another OS), .env files, and git history from ever reaching the daemon. Tests are deliberately not excluded, because the test stage needs them. The runtime stage copies only src/, so tests still never reach production.

# Dependencies & build output (always rebuilt inside the image)
node_modules
npm-debug.log*
.npm
coverage
.nyc_output
.eslintcache
dist
tmp

# Secrets & local config: must never enter the build context
.env
.env.*
!.env.example
*.pem
*.key
*.crt
secrets/

# VCS & CI metadata
.git
.gitignore
.github
.gitlab-ci.yml

# Docker files themselves (changes to them shouldn't bust the COPY cache)
Dockerfile*
.dockerignore
docker-compose*.yml
compose*.yml

# Editor / OS noise
.vscode
.idea
*.swp
.DS_Store

# Docs
*.md
docs/

3. Build, tag, push, and run commands

# ---- Variables (run from the repo root) ----
IMAGE=ghcr.io/vongu3/invoice-sync-api
VERSION=$(node -p "require('./package.json').version")   # e.g. 1.4.2
MINOR=${VERSION%.*}                                        # 1.4
MAJOR=${VERSION%%.*}                                       # 1
GIT_SHA=$(git rev-parse HEAD)
SHORT_SHA=$(git rev-parse --short=7 HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# ---- Log in to GHCR (PAT or GITHUB_TOKEN needs write:packages) ----
echo "$CR_PAT" | docker login ghcr.io -u vongu3 --password-stdin

# ---- Optional: run the CI test gate ----
DOCKER_BUILDKIT=1 docker build --target test .

# ---- Build the production image ----
# Add --build-arg SHARP_LIBVIPS=system for the system-libvips variant.
DOCKER_BUILDKIT=1 docker build \
  --platform linux/amd64 \
  --target runtime \
  --build-arg VERSION="$VERSION" \
  --build-arg GIT_SHA="$GIT_SHA" \
  --build-arg BUILD_DATE="$BUILD_DATE" \
  -t "$IMAGE:$VERSION" \
  .

# ---- Tag ----
docker tag "$IMAGE:$VERSION" "$IMAGE:sha-$SHORT_SHA"       # every build (traceability)

# Only move rolling tags for stable releases (not 1.5.0-rc.1, etc.)
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  docker tag "$IMAGE:$VERSION" "$IMAGE:$MINOR"               # 1.4
  docker tag "$IMAGE:$VERSION" "$IMAGE:$MAJOR"               # 1
fi
if [[ "$BRANCH" == "main" ]]; then
  docker tag "$IMAGE:$VERSION" "$IMAGE:latest"               # main only
fi

# ---- Push all local tags for this repo ----
docker push --all-tags "$IMAGE"

Run it in production style. Secrets are passed as -e VAR without a value, so Docker reads them from your shell environment. They don't appear in shell history or ps output.

docker run -d --name invoice-sync-api \
  -p 3000:3000 \
  --read-only \
  --tmpfs /app/tmp:rw,noexec,nosuid,size=256m,uid=1000,gid=1000,mode=0700 \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --memory 512m --cpus 1 \
  -e DATABASE_URL \
  -e REDIS_URL \
  -e JWT_SECRET \
  -e STRIPE_API_KEY \
  "$IMAGE:$VERSION"

# Verify
docker inspect --format '{{.State.Health.Status}}' invoice-sync-api   # -> healthy
curl -s localhost:3000/healthz                                         # -> {"status":"ok"}

Local development (the inspector is available on 9229 in this target only):

docker build --target dev -t invoice-sync-api:dev .
docker run --rm -p 3000:3000 -p 127.0.0.1:9229:9229 -v "$PWD/src:/app/src" \
  --env-file .env invoice-sync-api:dev

In Kubernetes, the equivalent settings are:

  • securityContext: {runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}}
  • an emptyDir: {medium: Memory} volume mounted at /app/tmp
  • secrets through secretKeyRef
  • liveness and readiness probes on GET /healthz

4. Expected image size

Stage / variant Approx. size (uncompressed)
node:20.11.1-bookworm builder (discarded) ~1.1 GB+
runtime, SHARP_LIBVIPS=bundled ~230–270 MB (~75–90 MB compressed in the registry)
runtime, SHARP_LIBVIPS=system ~400–450 MB

How the size was reduced:

  • Slim runtime base. It saves roughly 850 MB compared with the full bookworm image.
  • No compilers, headers, or python3 in the final image. They exist only in discarded stages.
  • Production dependencies only. --omit=dev drops jest, eslint, nodemon, and supertest, which usually make up 50–70% of node_modules.
  • npm, corepack, and yarn removed. About 20 MB saved.
  • No leftover caches. BuildKit cache mounts keep the npm and apt caches out of every layer, and the runtime apt lists are deleted in the same layer they were created.
  • Minimal app files. The explicit COPY src/ plus .dockerignore keep tests, docs, and git history out.

These figures are estimates from typical base-image and sharp sizes. I haven't built the image against your repo. Run docker images and docker history $IMAGE:$VERSION to get real numbers, and dive to inspect individual layers.


Assumptions to confirm

  1. Entry point is src/server.js and all runtime code lives under src/. If you have other runtime directories (such as config/, migrations/, or templates/), add a COPY line for each.
  2. Plain JavaScript with no build step. If you use TypeScript or a bundler, I'd add a build stage that runs npm run build and copies dist/ instead of src/.
  3. npm run lint and npm test exist (used by the test target).
  4. amd64 only, matching the linux-x64 target in your spec. For arm64 (Graviton or Apple Silicon hosts), use docker buildx build --platform linux/amd64,linux/arm64. sharp ships arm64 binaries too.
  5. GitHub repo URL in the org.opencontainers.image.source label is assumed to be github.com/vongu3/invoice-sync-api. GHCR uses it to link the package to the repo.
  6. Pin by digest in the long run. Tag pins like 20.11.1-bookworm-slim can still be re-pushed with Debian security patches. For byte-for-byte reproducibility, add @sha256:… to both FROM lines and let Renovate or Dependabot bump them.
  7. Node 20.11.1 is out of support. Node 20 LTS reached end-of-life in April 2026, so this pinned patch no longer receives security fixes. Plan a move to Node 22 or 24 LTS. The only change needed in this Dockerfile is the NODE_VERSION arg.

Related prompts

IT & Administration

Cloud Infrastructure Architect

Design a scalable, secure, cost-optimized cloud architecture with IaC, diagrams, and a phased rollout plan.

IT & Administration

Cybersecurity Audit Specialist

Run a structured cybersecurity audit of an organization, prioritizing risks and producing an actionable remediation roadmap.

IT & Administration

DevOps Automation Specialist

Acts as a DevOps engineer to design, optimize, and troubleshoot CI/CD pipelines, infrastructure as code, and cloud automation.

IT & Administration

Ansible Automation Playbook Creator

Generates production-ready, idempotent Ansible playbooks and roles for any infrastructure automation or configuration task.