Docker: van ontwikkeling naar productie

Complete Docker-gids voor het containeriseren van applicaties. Dockerfile, Docker Compose, multi-stage builds en productie-deployment met praktische voorbeelden.

Docker-gids van ontwikkeling naar productie

Docker revolutioneert de manier waarop applicaties worden ontwikkeld, getest en uitgerold. Door een applicatie en haar afhankelijkheden in een draagbare container te verpakken, elimineert Docker het beruchte "het werkt op mijn machine"-probleem en garandeert consistentie in alle omgevingen. Deze gids behandelt het complete traject van het eerste Dockerfile tot productie-deployment.

Docker in 2026

Docker Desktop 5.x brengt aanzienlijke prestatieverbeteringen, waaronder native containerd-ondersteuning, geoptimaliseerd resourcebeheer en naadloze Kubernetes-integratie. Multi-architectuur images (ARM/x86) zijn inmiddels standaardpraktijk.

Grondbeginselen van containerisatie

Een container is een lichtgewicht software-eenheid die code, runtime, systeembibliotheken en instellingen bundelt. In tegenstelling tot virtuele machines die hardware virtualiseren, delen containers de kernel van het hostsysteem, waardoor ze sneller opstarten en minder resources verbruiken.

bash
# terminal
# Docker installation on Ubuntu
sudo apt update
sudo apt install -y docker.io

# Add user to docker group (avoids sudo)
sudo usermod -aG docker $USER

# Verify installation
docker --version
# Docker version 26.1.0, build 1234567

# First container: downloads image and runs
docker run hello-world

Dit commando downloadt het hello-world-image van Docker Hub en start een container die een bevestigingsbericht toont.

bash
# terminal
# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List downloaded images
docker images

# Remove a container
docker rm <container_id>

# Remove an image
docker rmi <image_name>

Deze basiscommando's beheren de levenscyclus van containers en images.

Het eerste Dockerfile aanmaken

Een Dockerfile bevat instructies voor het bouwen van een Docker-image. Elke instructie creëert een laag in het uiteindelijke image, wat caching en hergebruik mogelijk maakt.

dockerfile
# Dockerfile
# Base image: Node.js 22 on Alpine Linux (lightweight)
FROM node:22-alpine

# Set working directory in the container
WORKDIR /app

# Copy dependency files first (cache optimization)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port (documentation)
EXPOSE 3000

# Startup command
CMD ["node", "server.js"]

De volgorde van instructies is cruciaal voor cache-optimalisatie. Bestanden die zelden veranderen (package.json) moeten vóór de broncode worden gekopieerd.

bash
# terminal
# Build image with a tag
docker build -t my-app:1.0 .

# Run the container
docker run -d -p 3000:3000 --name my-app-container my-app:1.0

# Check logs
docker logs my-app-container

# Access container shell
docker exec -it my-app-container sh

De flag -d draait de container op de achtergrond, -p mapt poort 3000 van de container naar poort 3000 van de host.

Alpine vs Debian

Alpine-images zijn aanzienlijk kleiner (ongeveer 5 MB tegenover 120 MB voor Debian). Ze gebruiken echter musl libc in plaats van glibc, wat incompatibiliteiten kan veroorzaken met sommige native afhankelijkheden. Bij problemen verdient het de voorkeur om Debian-gebaseerde images (node:22-slim) te gebruiken.

Multi-stage builds voor productie

Multi-stage builds creëren geoptimaliseerde productie-images door de build-omgeving te scheiden van de runtime-omgeving. Alleen de noodzakelijke artefacten worden in het uiteindelijke image opgenomen.

dockerfile
# Dockerfile.production
# ============================================
# Stage 1: Build
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Copy and install dependencies (including devDependencies)
COPY package*.json ./
RUN npm ci

# Copy source code
COPY . .

# Build the application (TypeScript, bundling, etc.)
RUN npm run build

# ============================================
# Stage 2: Production
# ============================================
FROM node:22-alpine AS production

# Non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy only necessary files from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Switch to non-root user
USER nodejs

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Startup command
CMD ["node", "dist/server.js"]

Deze aanpak vermindert de grootte van het uiteindelijke image aanzienlijk door build-tools, devDependencies en bronbestanden uit te sluiten.

bash
# terminal
# Build with specific file
docker build -f Dockerfile.production -t my-app:production .

# Compare image sizes
docker images | grep my-app
# my-app    production    abc123    150MB
# my-app    1.0           def456    450MB

De groottereductie kan 60-70% bereiken afhankelijk van het project, wat de deployment-tijden verbetert en het aanvalsoppervlak verkleint.

Docker Compose voor lokale orchestratie

Docker Compose vereenvoudigt het beheer van multi-container applicaties. Een YAML-bestand declareert alle services, hun configuraties en afhankelijkheden.

yaml
# docker-compose.yml
version: "3.9"

services:
  # Main application
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      # Mount source code for hot-reload
      - ./src:/app/src
      - ./package.json:/app/package.json
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      # Data persistence
      - postgres_data:/var/lib/postgresql/data
      # Initialization script
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis cache
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

# Named volumes for persistence
volumes:
  postgres_data:
  redis_data:

# Dedicated network for isolation
networks:
  app-network:
    driver: bridge

Services communiceren via hun namen (db, cache) door het interne Docker-netwerk. Healthchecks zorgen ervoor dat afhankelijkheden gereed zijn voordat de applicatie start.

bash
# terminal
# Start all services
docker compose up -d

# View logs from all services
docker compose logs -f

# Logs from a specific service
docker compose logs -f app

# Stop and remove containers
docker compose down

# Removal including volumes (caution: data loss)
docker compose down -v

# Rebuild after Dockerfile changes
docker compose up -d --build

Klaar om je DevOps gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Beheer van secrets en omgevingsvariabelen

Veilig beheer van secrets is cruciaal in productie. Docker biedt verschillende benaderingen afhankelijk van de context.

yaml
# docker-compose.override.yml (development only)
version: "3.9"

services:
  app:
    env_file:
      - .env.development
    environment:
      - DEBUG=true

Voor productie bieden Docker secrets meer veiligheid.

yaml
# docker-compose.production.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.production
    secrets:
      - db_password
      - api_key
    environment:
      - NODE_ENV=production
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

De applicatiecode leest secrets uit gemounte bestanden.

config/secrets.jsjavascript
const fs = require('fs');
const path = require('path');

// Utility function to read Docker secrets
function readSecret(secretName) {
  const secretPath = `/run/secrets/${secretName}`;

  // Check if secret file exists
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }

  // Fallback to classic environment variables
  const envVar = secretName.toUpperCase();
  return process.env[envVar];
}

module.exports = {
  databasePassword: readSecret('db_password'),
  apiKey: readSecret('api_key'),
};

Deze aanpak voorkomt het blootstellen van secrets in omgevingsvariabelen of Docker-images.

Optimalisatie van Docker-images

Verschillende technieken verkleinen de image-grootte en verbeteren de prestaties.

dockerfile
# Dockerfile.optimized
FROM node:22-alpine AS base

# Install necessary tools in a single layer
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

# ============================================
# Stage: Dependencies
# ============================================
FROM base AS deps

WORKDIR /app

# Copy only lock files for caching
COPY package.json package-lock.json ./

# Install with mounted npm cache (BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ============================================
# Stage: Builder
# ============================================
FROM base AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ============================================
# Stage: Production
# ============================================
FROM base AS production

# Image metadata
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production-ready Node.js application"

# Non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy production dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copy build output
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production

# dumb-init as PID 1 for signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Het gebruik van dumb-init zorgt voor correcte Unix-signaalafhandeling, wat een graceful shutdown van de container mogelijk maakt.

bash
# terminal
# Enable BuildKit for advanced features
export DOCKER_BUILDKIT=1

# Build with cache and detailed output
docker build --progress=plain -t my-app:optimized .

# Analyze image layers
docker history my-app:optimized

# Detailed image inspection
docker inspect my-app:optimized
Image-beveiliging

Images moeten regelmatig worden gescand op kwetsbaarheden met tools zoals Trivy of Snyk. Basis-images moeten regelmatig worden bijgewerkt om beveiligingspatches op te nemen.

Geavanceerde Docker-netwerken

Docker biedt verschillende netwerkdrivers voor uiteenlopende gebruiksscenario's.

yaml
# docker-compose.networking.yml
version: "3.9"

services:
  # Publicly accessible frontend
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network

  # API accessible only from frontend
  api:
    build: ./api
    networks:
      - backend-network
      - database-network
    # No external ports exposed

  # Isolated database
  database:
    image: postgres:16-alpine
    networks:
      - database-network
    # Accessible only by API

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
    internal: true  # No internet access
  database-network:
    driver: bridge
    internal: true

Deze configuratie isoleert services volgens het principe van minimale rechten. De database is alleen bereikbaar via de API.

bash
# terminal
# Inspect Docker networks
docker network ls

# Details of a specific network
docker network inspect app-network

# Create a custom network
docker network create --driver bridge --subnet 172.28.0.0/16 custom-network

# Connect a container to an existing network
docker network connect custom-network my-container

Volumes en datapersistentie

Docker-volumes bewaren data voorbij de levenscyclus van een container.

yaml
# docker-compose.volumes.yml
version: "3.9"

services:
  app:
    image: my-app:latest
    volumes:
      # Named volume for persistent data
      - app_data:/app/data
      # Bind mount for development
      - ./uploads:/app/uploads:rw
      # Read-only mount for configuration
      - ./config:/app/config:ro

  backup:
    image: alpine
    volumes:
      # Access same volume for backups
      - app_data:/data:ro
      - ./backups:/backups
    command: |
      sh -c "tar czf /backups/backup-$$(date +%Y%m%d).tar.gz /data"

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      device: /path/to/host/data
      o: bind

Het onderscheid tussen benoemde volumes en bind mounts is belangrijk: volumes worden beheerd door Docker, terwijl bind mounts direct het bestandssysteem van de host gebruiken.

bash
# terminal
# List volumes
docker volume ls

# Inspect a volume
docker volume inspect app_data

# Remove orphaned volumes
docker volume prune

# Backup a volume
docker run --rm -v app_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/volume-backup.tar.gz /data

Productie-deployment

Een robuuste deployment-workflow omvat build, tests en push naar een registry.

bash
# deploy.sh
#!/bin/bash
set -e

# Variables
REGISTRY="registry.example.com"
IMAGE_NAME="my-app"
VERSION=$(git describe --tags --always)

echo "Building version: $VERSION"

# Build production image
docker build \
  -f Dockerfile.production \
  -t $REGISTRY/$IMAGE_NAME:$VERSION \
  -t $REGISTRY/$IMAGE_NAME:latest \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=$VERSION \
  .

# Security scan
echo "Running security scan..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $REGISTRY/$IMAGE_NAME:$VERSION

# Push to registry
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest

echo "Deployment complete: $REGISTRY/$IMAGE_NAME:$VERSION"

Voor serverdeployments past een apart productie-composebestand de configuratie aan.

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/my-app:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Deze configuratie definieert resource-allocatie, update-strategie en healthchecks voor een betrouwbare deployment.

bash
# terminal
# Production deployment with Docker Compose
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Zero-downtime update (rolling update)
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

# Rollback if issues occur
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps \
  --scale app=0 && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

Monitoring en debugging van containers

Container-monitoring is essentieel in productie.

bash
# terminal
# Real-time statistics for all containers
docker stats

# Statistics for a specific container with custom format
docker stats my-app --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Inspect processes in a container
docker top my-app

# Real-time Docker events
docker events --filter container=my-app

# Copy files from/to a container
docker cp my-app:/app/logs/error.log ./error.log

Voor diepgaande debugging zijn verschillende technieken beschikbaar.

bash
# terminal
# Interactive shell in a running container
docker exec -it my-app sh

# Execute a single command
docker exec my-app cat /app/config/settings.json

# Start a container in debug mode
docker run -it --rm --entrypoint sh my-app:latest

# Inspect environment variables
docker exec my-app printenv

# Analyze logs with filters
docker logs my-app --since 1h --tail 100 | grep ERROR
yaml
# docker-compose.monitoring.yml
version: "3.9"

services:
  app:
    # ... existing configuration
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
      - "prometheus.path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret

volumes:
  prometheus_data:
  grafana_data:

Deze monitoring-stack maakt het mogelijk om container-metrics te verzamelen en te visualiseren.

Conclusie

Docker transformeert de ontwikkelcyclus door consistentie te garanderen in alle omgevingen. Containerisatie brengt draagbaarheid, isolatie en reproduceerbaarheid — essentiële eigenschappen voor moderne applicaties.

Docker-checklist voor productie

  • ✅ Multi-stage builds voor geoptimaliseerde images
  • ✅ Niet-root gebruiker in containers
  • ✅ Healthchecks geconfigureerd voor alle services
  • ✅ Secrets beheerd via Docker secrets of veilige omgevingsvariabelen
  • ✅ Resourcelimieten (CPU, geheugen) gedefinieerd
  • ✅ Volumes voor kritieke datapersistentie
  • ✅ Gecentraliseerde logging met bestandsrotatie
  • ✅ Beveiligingsscan van images voor deployment
  • ✅ Update-strategie zonder downtime
  • ✅ Geïsoleerd netwerk tussen services

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Docker beheersen is een fundamentele vaardigheid voor elke moderne ontwikkelaar. Van de lokale omgeving tot productie-deployment, Docker standaardiseert workflows en vereenvoudigt operaties. De hier gepresenteerde concepten vormen een solide basis voor het verkennen van Kubernetes en containerorchestratie op grote schaal.

Tags

#docker
#containerization
#devops
#docker compose
#deployment

Delen

Gerelateerde artikelen