# 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. - Published: 2026-01-21 - Updated: 2026-04-10 - Author: SharpSkill - Tags: docker, containerization, devops, docker compose, deployment - Reading time: 18 min --- 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 # Remove an image docker rmi ``` 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 ``` ## 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. ```javascript // config/secrets.js 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 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/devops/docker-from-development-to-production