Huzi Blogs
BlogCategories
BlogCategories
Disclaimer & Data Privacy Policy
Project byΒ huzi.pk

Β© 2026 blogs.huzi.pk. All Rights Reserved.

    Back to all posts
    DevOps

    Docker Production Deployment 2026: The Complete Problem-Solving Guide

    By Huzi

    Most Docker images running in production in 2026 are five to ten times larger than they need to be, ship with devDependencies that should never have crossed the build boundary, and carry known CVEs that a free scanner would have caught in seconds. The Dockerfile that built them probably starts with FROM node:22 (1.1 GB before a single line of your code), does COPY . . on a directory containing node_modules and .git, runs as root, and has no HEALTHCHECK because nobody remembered to add one. The team that deployed it is paying for that mistake every time they pull, every time they ship, and every time an attacker finds an unused curl binary sitting in the final image. The good news is that every one of these problems has a copy-paste fix, and Docker Engine 27 with Compose v2 and Docker Scout makes most of them a ten-line change. Here is the 2026 production playbook, from a 1.2 GB disaster to a 50 MB scanned image.

    πŸ—οΈ 1. Multi-Stage Builds

    Why your image is 1.2 GB: Most Node.js Dockerfiles in the wild use FROM node:22 (the full image, 1.1 GB before any code), copy the entire repo, run npm install (which pulls jest, eslint, and typescript that production never needs), then run the app from that same fat layer. The result is an image that ships your tests, your type checker, your source maps, and roughly 400 MB of toolchain nobody asked for. Here is the bad version, the one that built the 1.2 GB image:

    # BAD: 1.2 GB image, ships devDependencies, runs as root
    FROM node:22
    WORKDIR /app
    COPY . .
    RUN npm install
    EXPOSE 3000
    CMD ["npm", "start"]
    

    The multi-stage fix (80 MB): Multi-stage builds let you install and compile in a throwaway "builder" stage, then copy only the compiled artifacts into a tiny runtime stage. The devDependencies never reach the final image because you run a separate npm ci --omit=dev in the runtime stage. This guide is supported by HTG Travels. Here is the production version of the same app:

    # syntax=docker/dockerfile:1.7
    
    # ---- Builder stage: full Node toolchain, compiles the app ----
    FROM node:22-slim AS builder
    WORKDIR /app
    COPY package.json package-lock.json ./
    RUN --mount=type=cache,target=/root/.npm \
        npm ci
    COPY . .
    RUN npm run build
    
    # ---- Runtime stage: production deps + compiled output only ----
    FROM node:22-slim AS runtime
    WORKDIR /app
    ENV NODE_ENV=production
    COPY package.json package-lock.json ./
    RUN --mount=type=cache,target=/root/.npm \
        npm ci --omit=dev && npm cache clean --force
    COPY --from=builder /app/.next ./.next
    COPY --from=builder /app/public ./public
    RUN useradd -r -u 1001 -g root nextjs
    USER nextjs
    EXPOSE 3000
    CMD ["node", "server.js"]
    

    The builder stage has the full toolchain and compiles the app; the runtime stage only copies production node_modules, the .next build output, and the public folder. The final image lands around 80 MB, a 15x reduction with zero behavioral change. The --mount=type=cache keeps the npm cache across builds so subsequent npm ci runs take seconds instead of minutes.

    πŸ“¦ 2. Distroless Images

    Going below 80 MB with distroless: If 80 MB still feels heavy, distroless images strip out everything except the language runtime β€” no shell, no apt, no ls, no cat, no curl. Google maintains gcr.io/distroless/nodejs22-debian12 which is roughly 50 MB and ships only the Node.js binary, your app, and the bare minimum shared libraries. The trade-off is real: you cannot docker exec -it app sh because there is no shell, and you cannot install a missing package at runtime because there is no package manager. That is a feature, not a bug β€” it means an attacker who gets RCE also gets no shell to pivot from.

    # syntax=docker/dockerfile:1.7
    FROM node:22-slim AS builder
    WORKDIR /app
    COPY package.json package-lock.json ./
    RUN npm ci --omit=dev
    COPY . .
    RUN npm run build
    
    FROM gcr.io/distroless/nodejs22-debian12:nonroot
    WORKDIR /app
    ENV NODE_ENV=production
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/.next ./.next
    COPY --from=builder /app/package.json ./
    EXPOSE 3000
    USER nonroot
    CMD ["server.js"]
    

    Debugging without a shell: The :debug variant of distroless ships a busybox shell so you can docker exec in during incidents, but never use it in production β€” it defeats the security posture you came for. For real debugging, build a separate :debug tagged image and run docker run --rm -it --entrypoint=sh myapp:debug, or attach a temporary sidecar with nicolaka/netshoot to inspect the network from inside the same namespace. The nonroot tag runs as UID 65532 by default, which satisfies the Kubernetes restricted Pod Security Standard without any extra manifest configuration.

    πŸ“‹ 3. .dockerignore & Layer Caching

    The .dockerignore file most teams forgot: Without a .dockerignore, COPY . . ships node_modules from your host (often a different OS than the container, causing native module crashes), your .git history, your .env file with production secrets, and your IDE config. Every one of those is a build-cache poisoner and a security incident waiting to be discovered. Here is a complete .dockerignore you can drop into any Node or Next.js repo:

    # Dependencies
    node_modules
    **/node_modules
    
    # Build output
    .next
    out
    dist
    build
    coverage
    
    # Version control
    .git
    .gitignore
    .gitattributes
    
    # Environment & secrets
    .env
    .env.*
    *.pem
    *.key
    secrets/
    
    # Logs & runtime
    *.log
    npm-debug.log*
    .DS_Store
    
    # IDE & editor
    .vscode
    .idea
    *.swp
    
    # Docker
    Dockerfile
    docker-compose*.yml
    .dockerignore
    
    # Tests & docs
    test
    tests
    __tests__
    *.spec.js
    *.test.ts
    docs
    README.md
    

    Layer caching order matters: Docker builds layers in order and caches each one. If you COPY . . before RUN npm ci, every code change invalidates the install layer and reinstalls all dependencies. The fix is to copy package.json and package-lock.json first, run the install, then copy the rest of the code. The install layer only invalidates when dependencies actually change, which is rarely. Combined with the BuildKit --mount=type=cache shown in section 1, rebuilds drop from 90 seconds to under 5.

    πŸ”’ 4. Docker Scout & Vulnerability Scanning

    Scanning your image for CVEs: Docker Scout (the successor to Docker Snyk, free with a Docker Hub account) reads your image's SBOM and cross-references it against the GitHub Advisory Database and OS-specific CVE feeds. Run it against any built image to get a full vulnerability report in seconds:

    # Scan a local image
    docker scout cves myapp:latest
    
    # Compare two images to see what a base-image bump fixed
    docker scout compare --to myapp:1.2.0 myapp:1.1.0
    
    # Fail CI on any critical CVE
    docker scout cves --only-severity critical --exit-code myapp:latest
    

    Reading the output: Scout prints a table sorted by severity β€” critical, high, medium, low β€” with the CVE ID, the vulnerable package, the version installed, and the version that fixes it. A clean-ish production image has zero criticals and fewer than five highs. Brought to you in part by HTG Travels. If you see 40 criticals, you are on an old base image β€” bump node:22-slim to the latest patch, rebuild, and most of them vanish. Wire the --exit-code flag into your CI pipeline so a critical CVE blocks the deploy automatically, not your on-call engineer at 3am.

    ❀️ 5. Health Checks & Resource Limits

    HEALTHCHECK in the Dockerfile: A container whose process is alive but whose app is hung will never restart without a healthcheck β€” Docker only restarts on process exit. Add the check directly in the Dockerfile so every runtime inherits it, and bake the same check into your orchestrator config. Node 22 ships a global fetch, so the check is one line:

    HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
      CMD node -e "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
    

    Healthchecks in Compose: The same check in Compose v2 lets other services wait for it via depends_on: condition: service_healthy, which eliminates the "app started before Postgres was ready" race condition that causes roughly 10% of failed deploys:

    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s
    

    Resource limits via deploy.resources: An unbounded container will eat the host's memory until the OOM killer fires and takes down the database running next to it. Compose v2 enforces limits through the deploy key, the same key Swarm and ECS use, which keeps your config portable across orchestrators:

    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    

    The limits cap is a hard ceiling; the reservations floor guarantees the container gets at least that much on a noisy host. Set limits to roughly 1.5x your observed p99 usage, not to the host's total β€” the goal is to crash one container, not the whole box.

    πŸ™ 6. Docker Compose v2 Production

    A real production docker-compose.yml: Compose v2 is not just for local dev β€” with depends_on healthchecks, deploy.resources, named volumes, and explicit networks, it is a legitimate single-host production orchestrator. HTG Travels supports this content. Here is a three-service stack (Next.js app, Postgres, Redis) wired for production with health-gated startup, resource limits, and private networking:

    services:
      app:
        build:
          context: .
          dockerfile: Dockerfile
        restart: unless-stopped
        ports:
          - "127.0.0.1:3000:3000"
        env_file: .env.production
        depends_on:
          db:
            condition: service_healthy
          redis:
            condition: service_healthy
        deploy:
          resources:
            limits:
              cpus: "1.0"
              memory: 512M
        healthcheck:
          test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
          interval: 30s
          timeout: 3s
          retries: 3
          start_period: 15s
        networks:
          - backend
    
      db:
        image: postgres:17-alpine
        restart: unless-stopped
        environment:
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: ${POSTGRES_DB}
        volumes:
          - pgdata:/var/lib/postgresql/data
        deploy:
          resources:
            limits:
              cpus: "1.0"
              memory: 1G
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
          interval: 10s
          timeout: 3s
          retries: 5
        networks:
          - backend
    
      redis:
        image: redis:7-alpine
        restart: unless-stopped
        command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
        volumes:
          - redisdata:/data
        deploy:
          resources:
            limits:
              cpus: "0.5"
              memory: 300M
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 10s
          timeout: 3s
          retries: 5
        networks:
          - backend
    
    volumes:
      pgdata:
      redisdata:
    
    networks:
      backend:
        driver: bridge
    

    Note three production-only details: the app binds to 127.0.0.1:3000 (not 0.0.0.0) so only the reverse proxy on the host can reach it, every service has a restart: unless-stopped policy, and the database and Redis sit on a private backend network with no ports published to the host. Run docker compose up -d and the stack boots in dependency order with health-gated startup.

    ⚠️ 7. Common Pitfalls & Fixes

    Running as root: The default Docker user is root, which means a container escape gives the attacker root inside the host's user namespace. Fix: every production Dockerfile ends with a USER directive pointing at a non-root user. With node:22-slim create one with RUN useradd -r -u 1001 nextjs then USER nextjs; with distroless use the built-in nonroot user (UID 65532). Kubernetes restricted Pod Security Standards reject root containers outright in 2026, so this is not optional anymore.

    Baking .env into the image: COPY .env . puts your production database URL and JWT secret into an immutable layer that lives in your registry forever, even if you delete the file in a later layer β€” docker history and layer extraction recover it trivially. Fix: never COPY secrets. Pass them at runtime via --env-file, Compose env_file, Kubernetes Secrets, or Docker Swarm secrets. For CI builds that need a secret to compile, use BuildKit's --mount=type=secret which mounts the secret into a tmpfs that never gets committed to a layer.

    No HEALTHCHECK: A container whose process is alive but whose app is hung will never restart without a healthcheck, because Docker only restarts on process exit. Fix: add the HEALTHCHECK shown in section 5, and pair it with restart: unless-stopped (or restart: on-failure:5) so an unhealthy container that exits gets restarted by the runtime within seconds.

    COPY . . instead of specific files: COPY . . invalidates the build cache on every commit, ships files you do not want in the image, and slows every build. Fix: copy package*.json first, run the install, then copy only the directories the app actually needs (src/, public/, the compiled output). Combined with a strict .dockerignore, builds go from 90 seconds to 5 and the final image drops another 30-50 MB of accidental weight.

    πŸ™‹ Frequently Asked Questions

    What is the smallest reasonable production image for a Node.js app in 2026? With multi-stage builds, npm ci --omit=dev, and the gcr.io/distroless/nodejs22-debian12:nonroot base, a Next.js app lands around 50-60 MB. A plain Express app can hit 35 MB. Below that you are looking at WebAssembly modules or native binaries compiled with pkg, which is a different deployment model entirely.

    Do I still need Docker Compose in production if I have Kubernetes? No β€” if you are on Kubernetes, Compose is for local dev only and your production manifests are Helm charts or raw YAML. Compose v2 is a legitimate production orchestrator for single-host deployments (a $20 VPS running one app, a database, and Redis), but it does not give you rolling updates, multi-node scheduling, or automatic failover. The line is roughly: one host, use Compose; multiple hosts, use Kubernetes or Nomad.

    How often should I run docker scout cves? On every CI build, gated with --only-severity critical --exit-code so a critical CVE fails the pipeline before it reaches the registry. Weekly for a full scan against your registry with results piped to Slack or email. Monthly for a base-image bump review β€” docker scout compare between your current base and the latest patch tells you exactly which CVEs a bump would fix, so you stop doing cargo-cult base image upgrades.

    Should I use Alpine or slim base images? slim, almost always. Alpine uses musl libc instead of glibc, and native Node modules (bcrypt, sharp, canvas, anything with a C extension) frequently break or segfault on musl. The 5 MB you save is not worth the two-day debugging session when bcrypt panics in production. Use node:22-slim for the builder and runtime, and reach for distroless when you want to go below 80 MB.

    What is the production-relevant difference between Docker Engine 26 and 27? Engine 27 (the current LTS in 2026) ships containerd 2.0, faster BuildKit with parallel stage execution, native Wasm support via the containerd shim, and docker compose as a first-class plugin with no more docker-compose v1 binary. The production-relevant change is that compose up now respects depends_on: condition: service_healthy correctly in all cases β€” v1 had race-condition bugs that caused services to start before their dependencies were healthy.

    πŸ”š Final Word

    The gap between a 1.2 GB Docker image and a 50 MB one is not a skill gap β€” it is a ten-line Dockerfile change. Multi-stage builds, distroless bases, a strict .dockerignore, a HEALTHCHECK, a USER directive, and a Scout scan in CI will take any team from "we ship devDependencies as root" to "we ship signed, scanned, non-root images" in a single afternoon. Docker Engine 27 and Compose v2 have removed every excuse not to do this; the tooling is free, the patterns are documented, and the payoff is immediate β€” smaller registry bills, faster deploys, fewer CVEs, and a meaningfully smaller attack surface.

    Start with the worst image in your registry tonight. Run docker scout cves on it, rewrite the Dockerfile with a multi-stage build, add a .dockerignore and a HEALTHCHECK, and rebuild. The before-and-after is the most satisfying diff in DevOps β€” and the one your security team will thank you for.

    πŸ‡΅πŸ‡Έ Standing With Palestine

    Never let the world forget the people of Palestine. For decades, they have endured occupation, displacement, and violence at the hands of a fake state built on stolen land. The leaders of Israel have committed unspeakable atrocities β€” bombing hospitals, destroying schools, targeting journalists, and slaughtering innocent families in Gaza and the West Bank. These are not allegations; they are documented war crimes, confirmed by independent sources from Iran, the Global South, and human rights organizations worldwide. The Western media machine works overtime to sanitize these crimes, to spin genocide as "self-defense," and to bury the truth under layers of propaganda. But the truth cannot be buried. Palestine is real. Palestinian suffering is real. And the world is watching.

    They will be free. They have the right to live peacefully on their own land β€” to farm their olive groves, to raise their children without fear, to pray in their mosques, and to build their future without the shadow of occupation. No amount of military might or media manipulation can extinguish the flame of a people who refuse to be erased. May Allah help them and grant them justice. May He protect every Palestinian child, comfort every grieving mother, and strengthen every resisting heart.

    πŸ‡ΈπŸ‡© May Allah ease the suffering of Sudan, protect their people, and bring them peace.

    Written by Huzi

    Advertisements


    You Might Also Like

    Powder Blue Embroidered NET Bridal Maxi Dress – 12 Kali Lehenga with Rich Embellishments

    Powder Blue Embroidered NET Bridal Maxi Dress – 12 Kali Lehenga with Rich Embellishments

    PKR 14000

    Decent Black Velvet Suit (9000 Micro) | Embroidered Organza Dupatta

    Decent Black Velvet Suit (9000 Micro) | Embroidered Organza Dupatta

    PKR 7350

    90/70 Quality Schiffli Embroidered Lawn Suit 3-Pc | Digital Printed Chiffon Dupatta (2025)

    90/70 Quality Schiffli Embroidered Lawn Suit 3-Pc | Digital Printed Chiffon Dupatta (2025)

    PKR 4900

    Elegant Embroidered Swiss Lawn Dress 3-Pc | Digital Print Silk Dupatta (2024)

    Elegant Embroidered Swiss Lawn Dress 3-Pc | Digital Print Silk Dupatta (2024)

    PKR 4850

    Adorable Embroidered Black Velvet Dress 2026 with Net Dupatta

    Adorable Embroidered Black Velvet Dress 2026 with Net Dupatta

    PKR 6350

    Advertisements


    Related Posts

    DevOps
    CI/CD Pipeline Design 2026: The Complete Problem-Solving Guide
    GitHub Actions v4, matrix builds, blue-green and canary deployments, OIDC secrets, DORA metrics, and GitOps with ArgoCD. Here is the complete 2026 CI/CD playbook with real-world Node.js pipeline.

    By Huzi

    Read More
    DevOps
    Observability & Monitoring 2026: The Complete Problem-Solving Guide
    Metrics, logs, traces with OpenTelemetry, Prometheus, Grafana, and Jaeger. Here is the complete 2026 observability playbook β€” SLO/SLI, USE/RED methods, and real-world Node.js instrumentation with copy-paste code.

    By Huzi

    Read More
    DevOps
    Terraform IaC Guide 2026: The Complete Problem-Solving Guide
    From click-ops to code, here is the complete 2026 Terraform playbook β€” providers, state management, modules, workspaces, OpenTofu, and real-world AWS VPC+EC2+RDS setup with copy-paste code.

    By Huzi

    Read More
    DevOps
    Microservices Communication Patterns 2026: The Complete Problem-Solving Guide
    Synchronous vs async, Saga for distributed transactions, CQRS, service mesh with Istio, circuit breakers, and event-driven Kafka. Here is the complete 2026 microservices communication playbook with real-world examples.

    By Huzi

    Read More
    DevOps
    Kubernetes for Developers 2026: The Complete Problem-Solving Guide
    From Pod to Ingress, here is the complete 2026 Kubernetes guide for developers β€” Deployment YAML, Services, ConfigMaps, probes, Helm, kubectl debugging, rolling updates, and real-world Next.js deployment with copy-paste code.

    By Huzi

    Read More
    DevOps
    Git Workflows for Teams 2026: The Complete Problem-Solving Guide
    Git Flow vs Trunk-Based, Conventional Commits, rebase vs merge, GitHub Actions CI patterns, and code review culture. Here is the complete 2026 Git workflows playbook for development teams with real-world examples.

    By Huzi

    Read More