May 18, 2026·9 min·By

Docker in Production: Best Practices You're Probably Missing

DockerproductionsecurityDevOpscontainers

Most Docker tutorials teach you enough to get something running. This guide covers what it takes to run reliably in production.

Multi-Stage Builds

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build the app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production image (minimal)
FROM node:20-alpine AS runner
WORKDIR /app

# Don't run as root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/index.js"]

The result: a 150MB image instead of 1.2GB, and no dev dependencies or build tools in production.

Security Hardening

# Use specific versions, never 'latest' in production
FROM node:20.15.1-alpine3.20

# Drop all capabilities, add only what's needed
# (done via docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE)

# Read-only filesystem where possible
# (done via docker run --read-only --tmpfs /tmp)

# Scan for vulnerabilities
# Run: docker scout cves myimage:latest
# docker-compose.yml production hardening
services:
  app:
    image: myapp:latest
    user: "1001:1001"
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

Health Checks

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1
// /health endpoint in your app
app.get('/health', (req, res) => {
    // Check database connection
    db.query('SELECT 1').then(() => {
        res.json({ status: 'ok', uptime: process.uptime() });
    }).catch(() => {
        res.status(503).json({ status: 'degraded' });
    });
});

Without health checks, Docker marks containers as running even when they're serving 500 errors.

Zero-Downtime Rolling Updates

# docker-compose.yml
services:
  app:
    image: myapp:${VERSION}
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first  # Start new container before stopping old one
        failure_action: rollback
      rollback_config:
        parallelism: 1
        order: stop-first
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 10s
      retries: 3
      start_period: 15s

With order: start-first, Docker starts the new container, waits for it to pass health checks, then removes the old one. Zero-downtime.

Resource Limits

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

Without limits, a memory leak in one container can take down the entire host.

Logging

services:
  app:
    image: myapp:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Or send to a centralized logging system:

    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        tag: myapp

.dockerignore

node_modules
.git
.env
.env.*
*.test.js
coverage
docs
.github
*.md

A missing .dockerignore can add gigabytes of unnecessary context and leak secrets in build context.

Image Scanning in CI

# In GitHub Actions
      - name: Scan image for vulnerabilities
        uses: anchore/scan-action@v3
        with:
          image: myapp:${{ github.sha }}
          fail-build: true
          severity-cutoff: high

The difference between "it works in Docker" and "it runs reliably in production" is mostly these details: health checks, resource limits, non-root user, and zero-downtime update config.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

What tech stack does Innovibe use for projects like this?+

Next.js + TypeScript on the frontend, Node.js or Go on the backend, Postgres for the primary data store, and GCP (Cloud Run, BigQuery, Pub/Sub) for infrastructure. We pick tools that are boring in the best way — proven, well-documented, and easy to hire for.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation