Written by
Halkwinds Editorial Team
Halkwinds Research & Editorial

Docker and Containerization: From Development to Production
A practical guide to containerizing applications, writing production-grade Dockerfiles, and managing images securely.
If your team ships software in 2024, you are almost certainly running containers somewhere — or you are about to. Docker turned "it works on my machine" from a running joke into a solved problem, but the gap between a Dockerfile that works and one that is safe, fast, and reproducible in production is enormous. As an engineering manager, you are the person who owns that gap: the flaky builds, the 2GB images, the security review that stalls a release, and the on-call engineer who cannot figure out why the container behaves differently in staging. This guide walks through containerization end to end — from a first Dockerfile to production-grade image management — with the specific practices that separate teams who tolerate Docker from teams who genuinely benefit from it.
- Background / Why This Matters
- Prerequisites and Planning
- Step-by-Step Implementation
- Testing and Validation
- Common Mistakes / What to Avoid
- Frequently Asked Questions
- Conclusion
Background / Why This Matters
Containerization solves a deceptively simple problem: packaging an application together with its dependencies so it runs identically everywhere. Before Docker, teams spent enormous energy reconciling differences between developer laptops, CI runners, and production servers. Containers make the runtime environment part of the artifact itself.
For engineering managers, the payoff is organizational, not just technical:
- Faster onboarding. A new hire runs
docker compose upand has the full stack — database, cache, API — running in minutes instead of spending their first day fighting environment setup. - Predictable deployments. The image tested in CI is bit-for-bit the image that runs in production. Rollbacks become "deploy the previous tag" rather than a scramble.
- Portability across infrastructure. The same image runs on a single VM today and on a Kubernetes cluster tomorrow, without rewriting how the app is packaged.
The risk is that Docker feels easy to adopt and hard to do well. A working Dockerfile is trivial; a secure, lean, cache-optimized one that builds in 40 seconds instead of 8 minutes takes deliberate effort. Estimates vary, but a large share of production incidents in containerized environments trace back to configuration and image hygiene issues rather than the underlying platform.
Takeaway: Treat container images as first-class artifacts with the same rigor you apply to source code — versioned, reviewed, scanned, and owned by a team.
Prerequisites and Planning
Before you containerize anything, align on a few decisions. Getting these right upfront prevents rework across dozens of services later.
Technical prerequisites
- Docker Engine installed on developer machines and CI runners (Docker Desktop for macOS/Windows, or the engine directly on Linux).
- Docker Compose for local multi-service orchestration.
- A container registry — Docker Hub, GitHub Container Registry, Amazon ECR, or Google Artifact Registry — with access controls and image retention policies.
- A CI pipeline (GitHub Actions, GitLab CI, or similar) capable of building and pushing images.
Decisions to make before writing code
- Base image strategy. Will you standardize on
alpine,debian-slim, or distroless images? This affects image size, security surface, and debuggability. - Tagging convention. Avoid relying on
latest. Use immutable tags like the Git SHA (myapp:a1b2c3d) plus a movingstaging/productiontag. - Where orchestration lives. A single service on one host is fine with Docker Compose. Multiple services with scaling, self-healing, and rolling deploys point toward Kubernetes.
- Secret management. Decide early: secrets never belong baked into images. Plan for environment injection, a secrets manager, or Kubernetes Secrets.
Takeaway: Write these choices into a short internal standard document. Consistency across services is worth more than perfection in any single one.
Step-by-Step Implementation
We will containerize a typical Node.js API, but the patterns apply to Python, Go, Java, and most stacks.
Step 1: Write a multi-stage Dockerfile
Multi-stage builds separate the heavy build environment from the lean runtime image. This is the single most impactful technique for smaller, safer images.
# Build stage
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Note the key decisions: the build tools and dev dependencies never reach the final image, the container runs as the non-root node user, and dependency installation happens before copying source so Docker caches it between builds.
Step 2: Optimize layer caching
Docker builds are layered and cached top-to-bottom. Copy the files that change rarely (dependency manifests) before the files that change often (application source). If you copy everything first with COPY . . and then run npm ci, every code change busts the dependency cache and reinstalls everything — turning a 30-second build into several minutes.
Step 3: Add a .dockerignore
Prevent local clutter from bloating your build context and images:
node_modules
.git
.env
*.log
dist
coverage
Step 4: Define local orchestration with Docker Compose
For local development, wire your app to its dependencies:
services:
api:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on: [db]
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes: ["dbdata:/var/lib/postgresql/data"]
volumes:
dbdata:
Step 5: Build, tag, and push in CI
In your pipeline, build with the Git SHA and push to your registry:
docker build -t ghcr.io/acme/api:${GIT_SHA} .
docker push ghcr.io/acme/api:${GIT_SHA}
Step 6: Deploy to production
Choose the orchestration model that matches your scale. The table below compares the common paths.
| Approach | Best for | Effort | Scaling & self-healing |
|---|---|---|---|
| Docker Compose on a single host | Small apps, internal tools, MVPs | Low | Manual |
| Managed container service (ECS, Cloud Run) | SMBs wanting less ops overhead | Medium | Automatic, provider-managed |
| Kubernetes (EKS, GKE, self-hosted) | Multiple services, high scale, complex routing | High | Automatic, highly configurable |
Many teams over-adopt Kubernetes before they need it. If you run one or two services with modest traffic, a managed container service or Compose on a well-monitored host will move you faster. This is exactly the kind of trade-off our team at Halkwinds helps engineering teams navigate when building and deploying custom applications — matching the infrastructure to the actual workload rather than the hype.
Takeaway: Multi-stage builds, ordered layers, a .dockerignore, and immutable tags cover 80% of what separates amateur from production Docker.
Testing and Validation
A container that starts is not a container that is production-ready. Validate along these dimensions:
Functional validation
Run the built image locally — not just docker compose up against your source — to confirm the artifact behaves correctly: docker run --rm -p 3000:3000 ghcr.io/acme/api:test. Hit your health endpoint and exercise core flows.
Health checks
Add a container-level health check so orchestrators know when the app is genuinely ready, not just when the process exists:
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:3000/health || exit 1
Image scanning
Scan every image for known vulnerabilities before it reaches production. Tools like Trivy, Grype, or docker scout integrate into CI and can fail the build on critical CVEs. Run this on every merge to main, not just quarterly.
Size and build-time checks
Track image size over time (docker images) and build duration in CI. A sudden jump usually signals a broken cache or an accidental dependency. Set a soft alert if an image grows beyond an agreed threshold.
Takeaway: Bake functional tests, health checks, and vulnerability scanning into CI so a bad image cannot silently reach production.
Explore Further