{"post":{"slug":"how-to-write-a-dockerfile-2026","title":"How to write a Dockerfile in 2026: lean, fast, and secure","excerpt":"A bad Dockerfile ships a 2 GB image with root access and no layer cache. This 2026 guide shows you how to write lean, secure, production-ready Dockerfiles.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> Dockerfile is infrastructure code, and it deserves the same rigour as any other code. A naive Dockerfile ships a 1–2 GB image running as root with a 3-year-old base and credentials baked in. A well-written one ships a 50–150 MB image running as an unprivileged user with a minimal attack surface and a reliable layer cache. The difference is a handful of intentional choices.\n\n## What changed in 2026\n\n- **Rootless containers are the default.** Kubernetes 1.32+, ECS Fargate, and Cloud Run all enforce or strongly prefer non-root containers. Security scanners block root images by default in most enterprise pipelines.\n- **`docker buildx` and BuildKit are standard.** `BUILDKIT_INLINE_CACHE`, `--mount=type=cache`, and `--platform` multi-arch builds are first-class features you should be using.\n- **SBOM generation is expected.** Regulated industries and many enterprise customers require a Software Bill of Materials. Docker Scout and Syft integrate directly into the build pipeline.\n- **Base image sizes shrank.** `distroless`, `chainguard`, and wolfi-based images produce images in the 10–30 MB range for Go and Rust applications. Alpine remains the practical default for scripting runtimes.\n\n## The anatomy of a good Dockerfile\n\n```dockerfile\n# syntax=docker/dockerfile:1.7\nFROM node:22-alpine AS deps\nWORKDIR /app\n# Copy dependency manifests FIRST to maximise cache\nCOPY package.json package-lock.json ./\nRUN npm ci --omit=dev\n\n# ── Build stage ──────────────────────────────────────────\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\n# ── Runtime stage ────────────────────────────────────────\nFROM node:22-alpine AS runtime\nENV NODE_ENV=production\nWORKDIR /app\n\n# Non-root user\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\nUSER appuser\n\nCOPY --from=build --chown=appuser:appgroup /app/dist ./dist\nCOPY --from=deps  --chown=appuser:appgroup /app/node_modules ./node_modules\n\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]\n```\n\n## Multi-stage builds explained\n\nMulti-stage builds let you use a heavy image for building and a lean image for running.\n\n| Stage | Purpose | Included in final image |\n| --- | --- | --- |\n| `deps` | Install all dependencies | No |\n| `build` | Compile, transpile, bundle | No |\n| `runtime` | Run the application | Yes — only this stage ships |\n\nEverything in build stages (compilers, dev deps, test tools) is discarded. Only explicitly `COPY --from=<stage>` items make it into the final image.\n\n## Layer cache strategy\n\nDocker caches each layer. A layer is invalidated if its instruction or any file it depends on changes. Structure your Dockerfile so the most stable instructions come first:\n\n```dockerfile\n# 1. Base image — rarely changes\nFROM python:3.13-slim\n\n# 2. System dependencies — rarely changes\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    libpq-dev \\\n    && rm -rf /var/lib/apt/lists/*\n\n# 3. Python dependencies — changes when requirements.txt changes\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\n# 4. Application source — changes on every commit\nCOPY src/ ./src/\n```\n\nIf you put `COPY . .` before `RUN pip install`, every code change invalidates the pip cache. The ordering above means pip only re-runs when `requirements.txt` changes.\n\n## BuildKit cache mounts\n\nFor package managers with slow installs, use BuildKit cache mounts to persist the download cache across builds:\n\n```dockerfile\n# syntax=docker/dockerfile:1.7\nRUN --mount=type=cache,target=/root/.cache/pip \\\n    pip install -r requirements.txt\n\nRUN --mount=type=cache,target=/root/.npm \\\n    npm ci --omit=dev\n```\n\nThis keeps the package cache across builds on the same machine — dramatically faster rebuilds.\n\n## Security essentials\n\n**Non-root user:**\n```dockerfile\n# Alpine\nRUN addgroup -S app && adduser -S app -G app\nUSER app\n\n# Debian/Ubuntu\nRUN useradd --system --create-home --shell /sbin/nologin appuser\nUSER appuser\n```\n\n**Read-only filesystem (set in your orchestrator, not Dockerfile):**\n```yaml\n# kubernetes pod spec\nsecurityContext:\n  readOnlyRootFilesystem: true\n```\n\n**No secrets in build args or ENV:**\n```dockerfile\n# Bad — secret visible in image layers\nARG DB_PASSWORD\nENV DB_PASSWORD=${DB_PASSWORD}\n\n# Good — use runtime environment variables, not build-time\n# Or use --secret for build-time secrets\nRUN --mount=type=secret,id=db_password \\\n    cat /run/secrets/db_password | do_something\n```\n\n**Scan your image:**\n```bash\ndocker scout cves myapp:latest\n# or\ntrivy image myapp:latest\n```\n\n## Minimal Dockerfile by language\n\n### Python (FastAPI / Django)\n\n```dockerfile\nFROM python:3.13-slim AS runtime\nWORKDIR /app\nRUN useradd --system appuser\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY --chown=appuser . .\nUSER appuser\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\n### Go (single binary)\n\n```dockerfile\nFROM golang:1.23-alpine AS build\nWORKDIR /src\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 go build -o /app ./cmd/server\n\nFROM gcr.io/distroless/static-debian12\nCOPY --from=build /app /app\nUSER nonroot:nonroot\nENTRYPOINT [\"/app\"]\n```\n\nThe final Go image is ~5 MB with no shell, no package manager, and no OS utilities.\n\n## How to reduce image size\n\n1. Use `alpine` or `slim` variants over full Debian images.\n2. Use multi-stage builds — the single largest win.\n3. Combine RUN commands with `&&` to avoid intermediate layers.\n4. Clean up in the same `RUN` that installs: `apt-get install && rm -rf /var/lib/apt/lists/*`.\n5. Use `.dockerignore` to exclude `node_modules`, `.git`, test files, and docs.\n\n```\n# .dockerignore\nnode_modules\n.git\n**/*.test.ts\n**/*.spec.ts\ncoverage/\ndocs/\n.env\n```\n\n## Common mistakes\n\n**`COPY . .` before dependency install.** Kills the cache; every file change triggers a full reinstall.\n\n**Running as root.** Increases blast radius if the container is compromised. Almost no production workload requires it.\n\n**`CMD [\"sh\", \"-c\", \"node server.js\"]`** — wrapping in a shell means SIGTERM goes to sh, not node. Your app never gets the shutdown signal. Use exec form: `CMD [\"node\", \"server.js\"]`.\n\n**Ignoring `.dockerignore`.** Copying `node_modules` or `.git` into the build context slows every build and can leak secrets.\n\n## What to skip\n\n- **Installing git, curl, wget in the runtime image** unless your application genuinely requires them at runtime.\n- **`latest` tag as a base image** — it changes without notice and breaks reproducible builds.\n- **Secrets in ARG or ENV** — they are stored in image layers and visible with `docker history`.\n\n## FAQ\n\n**What is the right base image for Node.js?**\n`node:22-alpine` for most use cases. `node:22-slim` (Debian) if you need glibc for native modules. `distroless/nodejs22-debian12` for maximum security posture.\n\n**How do I make my image build faster in CI?**\nUse BuildKit cache mounts, push your build cache to a registry (`--cache-to type=registry`), and structure layers so stable instructions come first.\n\n**Should I use Docker Compose in production?**\nDocker Compose is fine for simple single-host deployments. For anything multi-host or requiring auto-scaling, use Kubernetes, ECS, or Cloud Run instead.\n\n**How do I handle database migrations in Docker?**\nRun migrations in an init container or as a separate step before deploying the new app container — never as part of the app startup `CMD`.\n\n## Where to go next\n\n- [How to set up CI/CD in 2026](/blog/how-to-set-up-ci-cd-2026)\n- [How to handle secrets in 2026](/blog/how-to-handle-secrets-2026)\n- [How to monitor a service in 2026](/blog/how-to-monitor-a-service-2026)","hue":310,"metaTitle":"How to Write a Dockerfile in 2026: Lean, Secure Images","metaDescription":"2026 guide to writing production Dockerfiles. Multi-stage builds, layer cache optimisation, non-root users, image size reduction, and security best practices.","keywords":["how to write a dockerfile 2026","dockerfile best practices 2026","multi-stage dockerfile","docker image size reduction","dockerfile security","docker layer cache","non-root docker container","production dockerfile guide","dockerfile node python 2026","lean docker image"],"tldr":["**Multi-stage builds are mandatory** — build dependencies never belong in the final image; they bloat size and expand the attack surface.","**Never run as root** — add a non-root user in your final stage; most container runtimes will warn or block root containers.","**Copy dependency files before source** — this maximises the Docker layer cache and eliminates reinstalling packages on every code change.","**Pin base image digests in production** — `FROM node:22-alpine` floats; `FROM node:22-alpine@sha256:abc123` does not.","**Skip:** installing dev tools, compilers, or test frameworks in the runtime stage — they add size and CVEs for no production benefit."],"readTime":"5 min read","wordCount":1111},"related":[{"slug":"how-to-containerize-an-app-2026","title":"How to containerize an app in 2026: Docker, layers, and production-ready images","excerpt":"Containerizing an app is now a baseline skill for every backend developer. This guide covers Dockerfiles, multi-stage builds, and production best practices.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310},{"slug":"how-to-reduce-docker-image-size-2026","title":"How to reduce Docker image size in 2026","excerpt":"Bloated Docker images slow pull times, increase attack surface, and cost more in registry storage. This guide covers the concrete techniques that actually shrink them in 2026.","category":"code","date":"February 26, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":312},{"slug":"containers-explained-2026","title":"Containers explained in 2026","excerpt":"Containers are the default unit of deployment in 2026. This guide explains how they work, when to use them, and how to keep images small and secure.","category":"code","date":"February 20, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-value-a-stock-2026","title":"How to value a stock in 2026: the methods that actually matter","excerpt":"Stock valuation is not guesswork — a handful of frameworks cover most cases. Here is how to value a stock in 2026 using the methods professionals rely on.","category":"finance","date":"April 20, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":280},"older":{"slug":"how-to-write-a-linkedin-summary-2026","title":"How to write a LinkedIn summary in 2026: the About section that gets results","excerpt":"Your LinkedIn About section is the first thing a recruiter reads after your headline. Here is how to write one in 2026 that earns a connection request rather than a scroll-past.","category":"guides","date":"April 20, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":125}}