DevOps & Cloud
Docker Container Guide: Dockerfile, Compose & Multi-Stage Builds
Docker eliminates inconsistencies between development, testing, and production environments by packaging your applications in isolated containers. With Dockerfile you define application images, Docker Compose orchestrates multiple services, and multi-stage builds produce production-ready, optimized images. This article provides a comprehensive guide from Docker container fundamentals to advanced practices.
Since I started using Docker in my projects, the "it works on my machine" problem has been completely eliminated. Ensuring a consistent working environment from development to production is one of Docker's greatest advantages.
Dockerfile: Image Creation Fundamentals
Basic Dockerfile Structure
A Dockerfile is an instruction file that defines how a Docker image is built. Each command creates a layer, and Docker caches these layers to speed up the build process.
# Optimized Dockerfile for .NET 8 API
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy project files first (cache optimization)
COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"]
COPY ["src/MyApi.Domain/MyApi.Domain.csproj", "src/MyApi.Domain/"]
COPY ["src/MyApi.Infrastructure/MyApi.Infrastructure.csproj", "src/MyApi.Infrastructure/"]
RUN dotnet restore "src/MyApi/MyApi.csproj"
# Copy source code and build
COPY . .
WORKDIR "/src/src/MyApi"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish \
--no-restore \
-p:PublishTrimmed=false \
-p:PublishReadyToRun=true
# Final stage - runtime only
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Run as non-root user
USER $APP_UID
ENTRYPOINT ["dotnet", "MyApi.dll"]Multi-Stage Build Advantages
Multi-stage builds dramatically reduce image size:
- Build stage: Contains the SDK and all development tools (~800MB)
- Final stage: Only runtime and compiled application (~200MB)
- Security: Build tools are not present in the production image
- Speed: Layer caching ensures fast rebuild times
.dockerignore: Excluding Unnecessary Files
The .dockerignore file defines files that should not be included in the Docker build context. A properly configured .dockerignore both shortens build time and prevents sensitive data from leaking into the image.
# .dockerignore
**/bin
**/obj
**/node_modules
**/.git
**/docker-compose*.yml
**/.env
**/.env.*
**/Dockerfile*
*.md
.vs
.vscode
.idea
**/.DS_Store
**/coverage
**/test-results
**/*.user
**/*.logWithout a .dockerignore file, Docker includes all files in the project in the build context. Large directories like .git and node_modules unnecessarily extend build time and can increase image size. In production environments, preventing .env files from being included in the build context is critical from a security perspective.
Docker Compose: Multi-Service Management
Docker Compose allows you to define and manage multiple services with a single YAML file. It is ideal for spinning up databases, caches, message queues, and API services together in development environments.
# docker-compose.yml - Full-stack development environment
version: '3.8'
services:
api:
build:
context: .
dockerfile: src/MyApi/Dockerfile
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=postgres;Database=myapp;Username=postgres;Password=postgres
- Redis__ConnectionString=redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- app-network
pgadmin:
image: dpage/pgadmin4
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
depends_on:
- postgres
networks:
- app-network
volumes:
postgres-data:
redis-data:
networks:
app-network:
driver: bridgeDocker Best Practices
Image Size Optimization
Smaller images deploy faster and consume fewer resources:
- Use Alpine-based images (e.g.,
node:20-alpine,postgres:16-alpine) - Use .dockerignore to exclude unnecessary files
- Layer ordering: Place frequently changing files at the end of the Dockerfile
- Multi-stage builds: Remove build tools from the final image
Security Practices
- Non-root user: Never run containers as root
- Specific tags: Use specific version tags instead of
latest - Secret management: Use Docker Secrets or Vault instead of environment variables for sensitive data
- Image scanning: Scan for vulnerabilities with Trivy or Snyk
Security Scanning for Image Validation
Scanning your Docker images for security vulnerabilities before deploying to production is a critical step. Trivy is a fast and comprehensive open-source security scanner:
# Scan image with Trivy
trivy image myapi:latest
# Show only CRITICAL and HIGH severity vulnerabilities
trivy image --severity CRITICAL,HIGH myapi:latest
# CI/CD pipeline usage - fail build if vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myapi:latest
# Filesystem scan (without Dockerfile)
trivy fs --security-checks vuln,config .By integrating scan results into your CI/CD pipeline, you prevent images with security vulnerabilities from reaching production. Establish a regular scanning routine to detect known CVEs in base images, and configure your pipeline to block deployments when critical issues are found.
Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1Health checks allow Docker to monitor container health. When used with depends_on in Docker Compose, they prevent startup issues by waiting for dependent services to be ready. In production, containers running without health checks will not be automatically restarted on failure and can silently go out of service.
Docker Networking
Docker networks manage inter-container communication. Different network types are optimized for different scenarios:
- Bridge: Default mode, isolated communication between containers on the same host
- Host: Container uses the host network directly, no port mapping required
- Overlay: Communication between multiple hosts in Docker Swarm mode
- None: Isolated operation without network access
# Create a custom bridge network
docker network create --driver bridge --subnet 172.20.0.0/16 app-network
# Connect a container to a specific network
docker run -d --name api --network app-network myapi:latest
# List containers in a network
docker network inspect app-network
# Test connectivity between two containers
docker exec api ping -c 3 postgresUser-defined bridge networks, unlike the default bridge, automatically support DNS-based service discovery. This allows containers to reach each other by name rather than IP address, which is particularly convenient in Compose environments.
Docker Volume Management
Volumes guarantee data persistence beyond the container lifecycle. In production environments, using volumes for database data, log files, and configuration files is mandatory.
# Create a named volume
docker volume create postgres-data
# Start container with volume
docker run -d --name db \
-v postgres-data:/var/lib/postgresql/data \
postgres:16-alpine
# Inspect volume details
docker volume inspect postgres-data
# Clean up unused volumes
docker volume prune -f
# Volume backup
docker run --rm -v postgres-data:/source -v $(pwd):/backup \
alpine tar czf /backup/postgres-backup.tar.gz -C /source .In development environments, bind mounts are used for source code sharing, while named volumes are preferred in production. Bind mounts depend on the host filesystem and are not portable, but they are ideal for reflecting code changes immediately during development.
Practical Tips and Summary
Keep these points in mind when working with Docker:
- Dev/Prod parity: Use docker-compose.yml for development and a separate docker-compose.prod.yml for production
- Log management: Write container logs to stdout/stderr and use centralized log collection tools
- Resource limits: Limit container resources with
--memoryand--cpusflags - CI/CD integration: Build Docker images in the CI pipeline and push to a container registry
- Layer caching: Use BuildKit cache in CI to reduce build times
Docker is an essential part of the modern software development process. Properly configured Dockerfile and Compose files both increase your development speed and make your deployment processes reliable.
Related Articles
AWS Cloud Infrastructure: EC2, S3, CloudFront & Lambda Guide
Learn AWS cloud services. EC2 server management, S3 file storage, CloudFront CDN, Lambda serverless functions, and VPC networking.
CI/CD Pipeline: Automated Testing & Deployment with GitHub Actions
CI/CD pipeline setup with GitHub Actions. Automated testing, builds, Docker image creation, and AWS deployment.
Microservice Architecture with Kubernetes: Pod, Service & Deployment
Microservice architecture with Kubernetes. Pod management, Service discovery, Deployment strategies, Ingress, and Helm charts.
Have a Flutter Project?
I build high-performance Flutter applications for iOS, Android, and web.
Get in Touch