Technology

Docker and Containerization Best Practices

Build smaller, faster, more secure containers. Dockerfile optimization, multi-stage builds, and security scanning.

All articles
TechnologyNexaEx TeamNovember 15, 2025 8 min read
Docker and Containerization Best Practices

Docker's Hidden Costs

Docker is elegant until your images are 2GB and take 10 minutes to build. Deployment slows. Security vulnerabilities hide in bloated base images. Production containers consume excessive memory.

Containerization best practices are the difference between a productive deployment pipeline and a slow, fragile one.

Multi-Stage Builds: Ship Only What You Need

Many teams ship the entire build environment in production images.

Bad Dockerfile:

FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o app main.go
CMD ["./app"]

This image includes the Go compiler, git, build tools, and source code. It's 1.5GB. Only the compiled binary is needed.

Good Dockerfile (multi-stage):

# Stage 1: Build
FROM golang:1.21 as builder
WORKDIR /app
COPY . .
RUN go build -o app main.go

# Stage 2: Runtime
FROM alpine:3.18
COPY --from=builder /app/app /app
CMD ["./app"]

Final image is 50MB. Alpine is tiny; Go binaries are static; no build tools needed at runtime.

Multi-stage benefits:

  • Smaller images: 50MB vs 1.5GB means faster pulls, faster deploys, less disk.
  • Security: production image only contains the app, no compiler or tools to exploit.
  • Faster deployments: CI/CD pipelines are 20-30x faster.

Layer Caching and Build Speed

Docker builds in layers. Each line in Dockerfile is a layer.

FROM python:3.11
COPY requirements.txt .          # Layer 1
RUN pip install -r requirements.txt  # Layer 2
COPY src/ .                      # Layer 3
RUN python setup.py build        # Layer 4

If you change src/, Docker rebuilds layers 3-4. Layers 1-2 are cached if unchanged.

Optimization:

  • Put dependencies (requirements.txt, package.json) before application code.
  • If dependencies change, pip/npm re-runs. Code changes don't trigger dependency reinstall.

Bad (slow):

COPY .  # Everything changes, everything rebuilds
RUN pip install -r requirements.txt

Good (cached):

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ .

Base Image Selection

Alpine (10MB): Tiny, minimal. Good for Go, Node, Python apps. No package manager cruft.

Debian/Ubuntu (200-500MB): Larger, includes apt, standard libraries. Easier debugging.

Distroless (10-50MB): Made by Google. Only app + minimal runtime. No shell, no package manager. Most secure; hardest to debug.

Recommendation: Alpine for Python/Node/Go. Distroless for Java/C# if security is critical.

Security Best Practices

Scan for vulnerabilities:

docker build -t myapp:latest .
trivy image myapp:latest  # Find CVEs

Run as non-root:

RUN useradd -m appuser
USER appuser
CMD ["./app"]

Don't run as root: If the container is compromised, root access gives attackers full system control.

Minimize secrets exposure:

# BAD: secret in image
RUN git clone https://token:secret@github.com/repo.git

# GOOD: use build secret (Docker BuildKit)
RUN --mount=type=secret,id=git_token     git clone https://$(cat /run/secrets/git_token)@github.com/repo.git

Use immutable tags for production:

# Avoid "latest" in production
docker push myapp:1.2.3  # Immutable
docker push myapp:latest  # Mutable, dangerous

"latest" is a lie. It's not repeatable. Dockerfile pinning is essential for reproducible builds.

Production Readiness

Health checks:

HEALTHCHECK --interval=30s --timeout=3s   CMD curl -f http://localhost:8080/health || exit 1

Kubernetes uses health checks to restart unhealthy containers.

Resource limits (in Kubernetes/Docker):

resources:
  requests:
    memory: "256Mi"
    cpu: "100m"
  limits:
    memory: "512Mi"
    cpu: "500m"

Without limits, one container can starve others of resources.

Logging:

# Container logs should go to stdout/stderr, not files
CMD ["python", "-u", "app.py"]  # -u = unbuffered

Docker captures stdout/stderr. Use "docker logs" to see output. Log files in containers are invisible to orchestrators.

Image Registry and Distribution

Push to a registry (Docker Hub, ECR, GCR). Tag with version and commit hash.

docker build -t myapp:1.2.3-a1b2c3d .
docker push myregistry.azurecr.io/myapp:1.2.3-a1b2c3d

This makes images reproducible. "Deploy version 1.2.3-a1b2c3d" is unambiguous.

Dockerfile Checklist

  • Multi-stage build if compiling
  • Minimal base image (Alpine, Debian, or Distroless)
  • Dependencies before application code
  • Non-root user for runtime
  • Health checks
  • No secrets in image
  • Immutable tags for production
  • Scanned for CVEs
  • Logs to stdout/stderr
  • Resource limits defined

Frequently asked questions

Should we use Docker Compose for production?

No. Docker Compose is for local development only. For production, use Kubernetes, Docker Swarm, or managed container services (Fargate, App Engine). These provide orchestration, scaling, and high availability that Compose doesn't.

How often should we rebuild images?

Rebuild on code changes (automatic via CI/CD). Rebuild monthly at minimum to patch base image vulnerabilities, even if your code hasn't changed. Use `docker build --pull` to always get the latest base image.

What's the difference between COPY and ADD in Dockerfile?

COPY copies files. ADD also extracts tar archives and downloads URLs. Use COPY (it's explicit). Use ADD only if you need extraction. Avoid ADD for URLs; it's unreliable and a security risk.

Let's build your next idea

One conversation to scope the work, meet the team, and get a proposal — usually within two business days.