CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

Home/DevOps/Docker for Developers: From Zero to Production-Ready
DevOps

Docker for Developers: From Zero to Production-Ready

A practical Docker guide for developers — from understanding containers to running multi-service apps with Docker Compose and preparing images for production deployment.

Sabir KhaloufiSabir KhaloufiMarch 20, 20264 min read

Docker solves one of the oldest problems in software: "it works on my machine." Once you understand how containers work and why they exist, you'll wonder how you shipped software without them.

This guide is for developers who've heard of Docker, maybe used it occasionally, but haven't fully integrated it into their workflow. By the end, you'll understand not just how to write Dockerfiles, but why each decision matters.

Why Containers?

Before Docker, deploying a Node.js app meant:

  1. SSH into a server
  2. Install the right Node.js version (which might conflict with other apps)
  3. Install dependencies
  4. Configure environment variables
  5. Set up a process manager (PM2, forever)
  6. Hope the OS packages match what you developed with

Docker packages your app and all its dependencies into a single image that runs identically everywhere — your laptop, CI, staging, production. The "it works on my machine" problem disappears because the machine is now part of the package.

Understanding the Core Concepts

  • Image: A snapshot of a filesystem with your app and dependencies. Read-only.
  • Container: A running instance of an image. Think process vs. program.
  • Dockerfile: Instructions for building an image.
  • Registry: A place to store and share images (Docker Hub, GitHub Container Registry).

Your First Dockerfile

Let's start with a Node.js API:

dockerfile
# Dockerfile
FROM node:20-alpine
 
WORKDIR /app
 
COPY package*.json ./
RUN npm ci
 
COPY . .
 
EXPOSE 3000
CMD ["node", "src/index.js"]

Build and run:

bash
docker build -t myapi:v1 .
docker run -p 3000:3000 myapi:v1

This works, but it has problems. Every code change rebuilds everything including npm ci. Let's understand layer caching first.

Docker Layer Caching

Every instruction in a Dockerfile creates a layer. Docker caches layers and only rebuilds from the first changed instruction downward.

dockerfile
# BAD: COPY . . before npm ci means code changes invalidate the npm ci cache
FROM node:20-alpine
WORKDIR /app
COPY . .          # This invalidates cache on every code change
RUN npm ci        # This always re-runs
dockerfile
# GOOD: Copy package files first, then source code
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./   # Only changes when deps change
RUN npm ci              # Cached unless package.json changes
COPY . .                # Code changes only affect this layer and below

This single ordering change can reduce build time from 2 minutes to 5 seconds on code-only changes.

Multi-Stage Builds

For production images, you want to be lean. Multi-stage builds let you use a full build environment and then copy only what's needed into the final image:

dockerfile
# Stage 1: Install and build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Stage 2: Production image
FROM node:20-alpine AS runner
WORKDIR /app
 
# Security: create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
 
# Only copy what's needed
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
 
USER appuser
 
EXPOSE 3000
ENV NODE_ENV=production
 
CMD ["node", "dist/index.js"]

The builder stage never ships to production. The final image contains only compiled output and production dependencies. This reduces image size from ~600MB to ~100MB and removes all dev tooling as an attack surface.

Docker Compose for Multi-Service Apps

Real applications have multiple services — an API, a database, maybe a cache. Docker Compose defines and runs them together:

yaml
# docker-compose.yml
version: '3.8'
 
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@postgres:5432/mydb
      - REDIS_URL=redis://redis:6379
    volumes:
      - ./src:/app/src  # Hot reload in dev
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
 
volumes:
  pgdata:
  redisdata:
bash
docker compose up -d        # Start everything in background
docker compose logs -f api  # Follow API logs
docker compose down         # Stop and remove containers
docker compose down -v      # Stop and remove containers + volumes (deletes DB data)

Development vs Production Compose

Use Compose files for different environments:

yaml
# docker-compose.dev.yml — override for development
version: '3.8'
 
services:
  api:
    build:
      target: builder     # Use the build stage with dev dependencies
    command: npm run dev  # Hot reload
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development
bash
# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
 
# Production (just the base file)
docker compose up -d

Essential .dockerignore

Never forget this file — it prevents unnecessary files from being sent to the Docker daemon:

code
# .dockerignore
node_modules
.git
.gitignore
dist
build
*.log
.env
.env.*
README.md
.DS_Store
coverage
.nyc_output

Without .dockerignore, you might be sending gigabytes of node_modules to Docker on every build.

Common Mistakes

1. Running containers as root. Always add a non-root user. If a container is compromised and runs as root, the attacker has root on that process.

2. Storing secrets in environment variables committed to git. Use Docker secrets, a vault solution, or at minimum a .env file that's gitignored.

3. Using latest tags in production. Pin to specific versions like node:20.11-alpine so deployments are reproducible.

4. Not using health checks. Docker Compose's depends_on without a health check only waits for the container to start, not for the service inside to be ready. Postgres takes a few seconds to be ready after the container starts.

5. Large image sizes. Use Alpine variants, multi-stage builds, and .dockerignore. A 600MB image takes 10x longer to pull than a 60MB one.

Useful Commands You'll Use Daily

bash
# See running containers
docker ps
 
# See all containers including stopped
docker ps -a
 
# Shell into a running container
docker exec -it container_name sh
 
# View logs
docker logs container_name -f --tail 100
 
# Remove all stopped containers, unused images, build cache
docker system prune -a
 
# Inspect a container's environment variables (useful for debugging)
docker inspect container_name | grep -A 20 '"Env"'

Key Takeaways

  • Docker solves environment inconsistency — build once, run anywhere
  • Layer order in Dockerfiles matters enormously for build performance
  • Multi-stage builds dramatically reduce production image size and attack surface
  • Docker Compose is the right tool for multi-service development environments
  • Never run production containers as root
  • Use health checks so dependent services wait for readiness, not just startup
#docker#containers#devops#docker compose#deployment
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

Full-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.

On this page

  • Why Containers?
  • Understanding the Core Concepts
  • Your First Dockerfile
  • Docker Layer Caching
  • Multi-Stage Builds
  • Docker Compose for Multi-Service Apps
  • Development vs Production Compose
  • Essential .dockerignore
  • Common Mistakes
  • Useful Commands You'll Use Daily
  • Key Takeaways

Related Articles

CI/CD Pipeline with GitHub Actions and Docker: A Complete Guide
DevOps

CI/CD Pipeline with GitHub Actions and Docker: A Complete Guide

Learn how to build a production-ready CI/CD pipeline using GitHub Actions and Docker. Covers testing, building images, pushing to registries, and deploying automatically.

Sabir KhaloufiMarch 25, 20265 min read
DevOps

Deploying Next.js Apps on AWS EC2: A Step-by-Step Guide

Learn how to deploy a Next.js application on AWS EC2 from scratch — covering server setup, Nginx reverse proxy, SSL certificates, PM2, and automated deployments.

Sabir KhaloufiMarch 15, 20264 min read
DevOps

Kubernetes Basics Every Developer Should Know

A practical Kubernetes introduction for developers — covering Pods, Deployments, Services, ConfigMaps, and the core concepts you need to deploy and manage containerized apps.

Sabir KhaloufiMarch 10, 20264 min read