DevOps & Cloud

Docker Container Guide: Dockerfile, Compose & Multi-Stage Builds

12 min readMarch 15, 2026
Docker rehberiDocker tutorialDockerfileDocker ComposeMulti-stage buildDocker konteynerKonteynerleştirmeDocker deploymentDocker .NETDocker production

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.

dockerfile
# 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
# .dockerignore
**/bin
**/obj
**/node_modules
**/.git
**/docker-compose*.yml
**/.env
**/.env.*
**/Dockerfile*
*.md
.vs
.vscode
.idea
**/.DS_Store
**/coverage
**/test-results
**/*.user
**/*.log

Without 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.

yaml
# 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: bridge

Docker 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:

bash
# 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

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Health 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
bash
# 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 postgres

User-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.

bash
# 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:

  1. Dev/Prod parity: Use docker-compose.yml for development and a separate docker-compose.prod.yml for production
  2. Log management: Write container logs to stdout/stderr and use centralized log collection tools
  3. Resource limits: Limit container resources with --memory and --cpus flags
  4. CI/CD integration: Build Docker images in the CI pipeline and push to a container registry
  5. 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

Have a Flutter Project?

I build high-performance Flutter applications for iOS, Android, and web.

Get in Touch