Skip to main content

Docker Deployment

This guide covers containerizing your application using Docker, setting up multi-container environments with Docker Compose, and deploying to production.

Overview

The architecture includes a multi-stage Dockerfile for optimized builds and supports deployment with:
  • SQL Server
  • MongoDB
  • RabbitMQ
  • Your .NET API

Understanding the Dockerfile

The included Dockerfile uses a multi-stage build for optimal image size and security:
# Base runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

# Build stage with SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Template-API/Template-API.csproj", "Template-API/"]
RUN dotnet restore "Template-API/Template-API.csproj"
COPY . .
WORKDIR "/src/Template-API"
RUN dotnet build "Template-API.csproj" -c Release -o /app/build

# Publish stage
FROM build AS publish
RUN dotnet publish "Template-API.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Final stage
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Template-API.dll"]
Reference: Template-API/Dockerfile

Dockerfile Stages

Uses the lightweight ASP.NET runtime image. Only includes what’s needed to run the application, reducing image size and attack surface.
Uses the full .NET SDK to compile the application. Restores NuGet packages and builds in Release configuration.
Creates the final published output optimized for deployment.
Copies only the published output to the runtime image, resulting in a minimal production image.

Building the Docker Image

1

Build the Image

From the solution root directory (containing the Dockerfile):
docker build -t hybrid-ddd-api:latest -f Template-API/Dockerfile .
Parameters:
  • -t hybrid-ddd-api:latest - Tag the image with name and version
  • -f Template-API/Dockerfile - Path to Dockerfile
  • . - Build context (solution root)
2

Verify the Build

Check that the image was created:
docker images | grep hybrid-ddd-api
You should see output like:
hybrid-ddd-api   latest   abc123def456   2 minutes ago   220MB
3

Run the Container

Run the container with environment variables:
docker run -d \
  --name hybrid-ddd-api \
  -p 8080:80 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e ConnectionStrings__SqlConnection="Server=host.docker.internal;Database=YourDB;User ID=sa;Password=YourPassword123;" \
  hybrid-ddd-api:latest
Parameters:
  • -d - Run in detached mode
  • --name - Container name
  • -p 8080:80 - Map port 8080 on host to port 80 in container
  • -e - Environment variables
4

Test the API

Verify the API is running:
curl http://localhost:8080/api/v1/DummyEntity
Or visit http://localhost:8080/swagger in your browser.

Docker Compose Setup

For a complete multi-container setup, create a docker-compose.yml file:
version: '3.8'

services:
  # SQL Server
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: hybrid-ddd-sqlserver
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong@Password123
      - MSSQL_PID=Developer
    ports:
      - "1433:1433"
    volumes:
      - sqlserver-data:/var/opt/mssql
    networks:
      - hybrid-network

  # MongoDB
  mongodb:
    image: mongo:7.0
    container_name: hybrid-ddd-mongodb
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=YourPassword123
    ports:
      - "27017:27017"
    volumes:
      - mongodb-data:/data/db
    networks:
      - hybrid-network

  # RabbitMQ
  rabbitmq:
    image: rabbitmq:3.12-management
    container_name: hybrid-ddd-rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=YourPassword123
    ports:
      - "5672:5672"   # AMQP port
      - "15672:15672" # Management UI
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - hybrid-network

  # Your API
  api:
    build:
      context: .
      dockerfile: Template-API/Dockerfile
    container_name: hybrid-ddd-api
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_URLS=http://+:80
      - Configurations__UseDatabase=sqlserver
      - ConnectionStrings__SqlConnection=Server=sqlserver;Database=HybridDDDArchitecture;User ID=sa;Password=YourStrong@Password123;TrustServerCertificate=True;
      - ConnectionStrings__MongoConnection=mongodb://admin:YourPassword123@mongodb:27017/HybridDDDArchitecture?authSource=admin
      - RabbitMqEventBus__Connectionstring=amqp://admin:YourPassword123@rabbitmq:5672/
    ports:
      - "8080:80"
    depends_on:
      - sqlserver
      - mongodb
      - rabbitmq
    networks:
      - hybrid-network

volumes:
  sqlserver-data:
  mongodb-data:
  rabbitmq-data:

networks:
  hybrid-network:
    driver: bridge

Running with Docker Compose

1

Start All Services

docker-compose up -d
This starts all containers in the background.
2

View Logs

# All services
docker-compose logs -f

# Specific service
docker-compose logs -f api
3

Check Service Status

docker-compose ps
You should see all services running:
NAME                    STATUS    PORTS
hybrid-ddd-api          Up        0.0.0.0:8080->80/tcp
hybrid-ddd-sqlserver    Up        0.0.0.0:1433->1433/tcp
hybrid-ddd-mongodb      Up        0.0.0.0:27017->27017/tcp
hybrid-ddd-rabbitmq     Up        0.0.0.0:5672->5672/tcp, 0.0.0.0:15672->15672/tcp
4

Access Services

5

Stop All Services

# Stop containers (preserves data)
docker-compose stop

# Stop and remove containers (preserves volumes)
docker-compose down

# Stop and remove everything including volumes
docker-compose down -v

Environment Configuration

Development Environment

Create docker-compose.override.yml for development settings:
version: '3.8'

services:
  api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    volumes:
      - ./Template-API:/app
    ports:
      - "5000:80"

Production Environment

Create docker-compose.prod.yml for production:
version: '3.8'

services:
  api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    restart: always
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G
Run with:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Production Deployment

1

Use Environment Variables

Never hardcode secrets. Use environment files:Create .env file:
SQL_SA_PASSWORD=YourStrong@Password123
MONGO_PASSWORD=YourMongoPassword123
RABBITMQ_PASSWORD=YourRabbitPassword123
ASPNETCORE_ENVIRONMENT=Production
Update docker-compose.yml:
services:
  sqlserver:
    environment:
      - SA_PASSWORD=${SQL_SA_PASSWORD}
  
  api:
    environment:
      - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
Important: Add .env to .gitignore!
2

Use Docker Secrets (Swarm)

For Docker Swarm:
# Create secrets
echo "YourStrong@Password123" | docker secret create sql_password -
echo "YourMongoPassword123" | docker secret create mongo_password -

# Use in docker-compose.yml
services:
  sqlserver:
    secrets:
      - sql_password
    environment:
      - SA_PASSWORD_FILE=/run/secrets/sql_password

secrets:
  sql_password:
    external: true
  mongo_password:
    external: true
3

Configure Health Checks

Add health checks to ensure services are ready:
services:
  api:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
  
  sqlserver:
    healthcheck:
      test: ["/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "$$SA_PASSWORD", "-Q", "SELECT 1"]
      interval: 30s
      timeout: 10s
      retries: 3
4

Set Up Reverse Proxy

Use Nginx as a reverse proxy:
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - hybrid-network
nginx.conf:
http {
    upstream api {
        server api:80;
    }

    server {
        listen 80;
        server_name yourdomain.com;

        location / {
            proxy_pass http://api;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}
5

Implement Logging

Configure centralized logging:
services:
  api:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
Or use a logging service like Seq, ELK, or Splunk.

Database Migrations in Docker

Apply Migrations on Startup

Modify your Program.cs to apply migrations automatically:
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();

    // Apply migrations
    using (var scope = host.Services.CreateScope())
    {
        var db = scope.ServiceProvider.GetRequiredService<StoreDbContext>();
        db.Database.Migrate();
    }

    host.Run();
}

Manual Migration with Init Container

Create a separate migration container:
services:
  migrations:
    build:
      context: .
      dockerfile: Template-API/Dockerfile
    command: ["/bin/sh", "-c", "dotnet ef database update --startup-project Template-API"]
    environment:
      - ConnectionStrings__SqlConnection=Server=sqlserver;Database=HybridDDDArchitecture;User ID=sa;Password=YourStrong@Password123;
    depends_on:
      - sqlserver
    networks:
      - hybrid-network

Monitoring and Debugging

Container Logs

# View logs
docker logs hybrid-ddd-api

# Follow logs in real-time
docker logs -f hybrid-ddd-api

# View last 100 lines
docker logs --tail 100 hybrid-ddd-api

Execute Commands in Container

# Open shell in container
docker exec -it hybrid-ddd-api /bin/bash

# Check environment variables
docker exec hybrid-ddd-api env

# View files
docker exec hybrid-ddd-api ls -la /app

Resource Usage

# View resource usage
docker stats

# View container details
docker inspect hybrid-ddd-api

Troubleshooting

Check logs:
docker logs hybrid-ddd-api
Common causes:
  • Missing environment variables
  • Database connection failure
  • Configuration errors
Solution: Verify all required environment variables are set and databases are accessible.
Error: Connection timeout or refusedSolutions:
  • Ensure database container is running: docker ps
  • Use container name as hostname: Server=sqlserver not localhost
  • Check network configuration
  • Wait for database to be ready (use health checks)
  • Verify firewall rules
Error: Bind for 0.0.0.0:8080 failed: port is already allocatedSolution:
# Find process using port
lsof -i :8080  # macOS/Linux
netstat -ano | findstr :8080  # Windows

# Change port in docker-compose.yml
ports:
  - "8081:80"  # Use different host port
Solutions:
  • Ensure you’re building from solution root
  • Verify .csproj file paths in Dockerfile
  • Check for missing dependencies
  • Clear Docker cache: docker builder prune
  • Build with verbose output: docker build --progress=plain ...

Best Practices

  • Never commit .env files
  • Use secrets management for production
  • Run containers as non-root user
  • Scan images for vulnerabilities: docker scan hybrid-ddd-api:latest
  • Keep base images updated
  • Use specific image tags, not latest
  • Use multi-stage builds to minimize image size
  • Leverage build cache with proper COPY ordering
  • Set resource limits for containers
  • Use volume mounts for development, not production
  • Enable connection pooling for databases
  • Tag images with version numbers
  • Document environment variables
  • Use docker-compose for multi-container apps
  • Implement health checks
  • Centralize logging
  • Regular backups of volumes

Next Steps

Database Setup

Learn about migrations and database configuration

Event Handling

Configure RabbitMQ for event-driven architecture

Build docs developers (and LLMs) love