{"post":{"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","content":"<span class=\"dropcap\">C</span>ontainerization is one of the most high-leverage skills in backend development in 2026. A good Docker image gives you reproducible builds, easy deploys to any cloud or Kubernetes cluster, and isolated dependencies that stop \"works on my machine\" arguments. A bad image is bloated, insecure, and slow to build. This guide shows you the difference.\n\n## What changed in 2026\n\n- **Docker Desktop 5.x** includes BuildKit by default and supports `docker build --cache-from` with OCI cache backends, making remote cache sharing straightforward.\n- **Distroless images** from Google and Chainguard are now a standard hardening choice — they contain only the runtime, no shell, no package manager.\n- **OCI Image Format** is the universal standard; `podman` and `nerdctl` are drop-in alternatives to the `docker` CLI.\n- **Sigstore / cosign** for image signing is expected in regulated environments; supply-chain security is a real concern in 2026.\n- **`docker init`** (introduced in Docker v24) generates a production-ready Dockerfile for common stacks — useful starting point.\n\n## Basic Dockerfile (Node example)\n\n```dockerfile\nFROM node:22-alpine\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm ci --only=production\n\nCOPY . .\n\nEXPOSE 3000\nCMD [\"node\", \"server.js\"]\n```\n\nThis works but has problems: it is single-stage, the image includes npm and the full Node image, and it runs as root.\n\n## Production multi-stage Dockerfile (Node)\n\n```dockerfile\n# ---- build stage ----\nFROM node:22-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# ---- runtime stage ----\nFROM node:22-alpine AS runtime\nWORKDIR /app\n\n# Create non-root user\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\n\nCOPY package*.json ./\nRUN npm ci --only=production && npm cache clean --force\n\nCOPY --from=builder /app/dist ./dist\n\nUSER appuser\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]\n```\n\nThe runtime image has no build tools, no source files, and runs as a non-root user.\n\n## Multi-stage Python example\n\n```dockerfile\nFROM python:3.12-slim AS builder\nWORKDIR /build\nCOPY requirements.txt .\nRUN pip install --prefix=/install -r requirements.txt\n\nFROM python:3.12-slim\nWORKDIR /app\nCOPY --from=builder /install /usr/local\nCOPY src/ ./src/\nRUN useradd -m appuser\nUSER appuser\nCMD [\"python\", \"-m\", \"src.main\"]\n```\n\n## .dockerignore — what to always exclude\n\n```\nnode_modules/\n.git/\n.env\n.env.*\n*.log\ndist/        # if you build inside Docker\n__pycache__/\n.pytest_cache/\n.coverage\nREADME.md\n```\n\nA missing `.dockerignore` can accidentally send hundreds of megabytes of context to the Docker daemon.\n\n## Image size comparison\n\n| Approach | Typical size (Node app) |\n| --- | --- |\n| `node:22` single stage | ~1.1 GB |\n| `node:22-alpine` single stage | ~180 MB |\n| Multi-stage, alpine runtime | ~80 MB |\n| Distroless runtime | ~50 MB |\n\nAlpine and multi-stage together get you 10–20× smaller than the naive baseline.\n\n## Layer caching strategy\n\nOrder Dockerfile instructions from least-changing to most-changing:\n\n```dockerfile\n# 1. Base image (cached for weeks)\nFROM node:22-alpine\n# 2. System deps (change rarely)\nRUN apk add --no-cache dumb-init\n# 3. Package manifest (changes when you add deps)\nCOPY package*.json ./\nRUN npm ci --only=production\n# 4. Source code (changes every commit)\nCOPY . .\n```\n\nIf you copy source before `npm ci`, every code change busts the dependency cache and reinstalls everything.\n\n## How to pick a base image\n\n| Base | When to use |\n| --- | --- |\n| `node:22-alpine` | Most Node apps; small, well-maintained |\n| `python:3.12-slim` | Python apps; slim drops unnecessary Debian packages |\n| `golang:1.23-alpine` as builder only | Build stage; ship the binary in `scratch` or `distroless` |\n| `gcr.io/distroless/base` | Hardened runtime, no shell — good for compiled languages |\n| `ubuntu:24.04` | Only if you need apt packages unavailable in alpine |\n\n## Common mistakes\n\n**No `.dockerignore`.** Node's `node_modules` folder alone can be hundreds of MB sent to the build daemon on every build.\n\n**Secrets baked into image layers.** Never `COPY .env` or `RUN export SECRET=...` — secrets are visible in `docker history`. Use build args only for non-secret config; pass real secrets at runtime via environment variables or secret mounts.\n\n**Single-stage builds with build tools in production.** Compilers, bundlers, and test runners bloat the image and expand the attack surface.\n\n**`latest` tag in FROM.** Pin to a specific version tag; CI pipelines that pull `latest` can silently break when a major version ships.\n\n**Not adding a health check.** Docker and Kubernetes need to know your container is healthy:\n\n```dockerfile\nHEALTHCHECK --interval=30s --timeout=5s \\\n  CMD curl -f http://localhost:3000/health || exit 1\n```\n\n## What to skip\n\n- **Installing an SSH server** inside a container — use `docker exec` or ephemeral debug containers instead.\n- **Running multiple processes** with a generic process manager like `supervisord` unless you have a very specific reason; prefer one process per container.\n- **Building images on production machines** — build in CI, push to a registry, pull to prod.\n\n## FAQ\n\n**How do I pass environment variables at runtime?**\nUse `docker run -e KEY=value` or `--env-file .env`. Never bake secrets into the image.\n\n**What is `dumb-init` and do I need it?**\n`dumb-init` is a minimal init that properly forwards signals and reaps zombie processes. Use it when your CMD is a Node or Python process that does not handle signals well on its own.\n\n**How do I reduce build times in CI?**\nEnable BuildKit (`DOCKER_BUILDKIT=1`), use `--cache-from` pointing to your registry, and structure the Dockerfile for maximum cache reuse (deps before source).\n\n**When should I use Podman instead of Docker?**\nPodman is daemonless and runs containers rootless by default, which is preferred in some enterprise Linux environments. The Dockerfile syntax is identical.\n\n## Where to go next\n\n- [How to write a GitHub Action in 2026](/blog/how-to-write-a-github-action-2026)\n- [How to set up Nginx in 2026](/blog/how-to-set-up-nginx-2026)\n- [How to reduce Docker image size in 2026](/blog/how-to-reduce-docker-image-size-2026)","hue":310,"metaTitle":"How to Containerize an App in 2026: Docker Best Practices","metaDescription":"Containerize an app in 2026 — write production Dockerfiles, use multi-stage builds, shrink image size, and run securely. Practical Docker guide.","keywords":["how to containerize an app 2026","docker multi-stage build 2026","dockerfile best practices 2026","docker production image","reduce docker image size","docker non-root user","containerize node app","docker layer caching","docker security 2026","container deployment 2026"],"tldr":["**Use multi-stage builds** — separate build and runtime stages cut final image size by 5–10x.","**Pin base image versions** — `node:22-alpine` not `node:latest`; unpinned images break on CI at the worst moment.","**Run as a non-root user** — add a `USER` instruction; containers running as root are a security liability.","**Copy only what the app needs** — use `.dockerignore` aggressively; `node_modules`, `.git`, and secrets have no place in an image.","**Skip:** installing a full OS toolchain in the runtime image — build in a builder stage, copy only the binary or dist folder."],"readTime":"4 min read","wordCount":950},"related":[{"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","readTime":"5 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-configure-vite-2026","title":"How to configure Vite in 2026","excerpt":"Vite 6 is the build tool of choice for modern JavaScript projects. This guide covers the configuration options that matter — aliases, env vars, proxies, plugins, and production tuning.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":318},"older":{"slug":"how-to-cut-the-cable-bill-2026","title":"How to cut the cable bill in 2026: cord-cutting, negotiation, and what to keep","excerpt":"The average cable and streaming bundle now costs more than the cable it replaced. Here is how to audit your subscriptions, cut cord-cable costs, and build only the entertainment stack you actually watch in 2026.","category":"finance","date":"April 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310}}