When building enterprise-grade applications using Clean Architecture, your codebase is naturally split across multiple projects: Domain, Application, Infrastructure, and API. While this separation provides immense maintainability and testability, it introduces unique challenges when it comes to containerization and deployment. A naive Dockerfile will fail or result in bloated images if it does not account for multi-project solution boundaries.

To take your application from code to production reliably, you need two things: a optimized multi-stage Dockerfile that respects your solution structure, and an automated GitHub Actions CI/CD pipeline that runs tests and pushes your container images automatically.

In this comprehensive guide, we will walk through containerizing a Clean Architecture ASP.NET Core project, orchestrating it locally with Docker Compose, and automating deployment workflows.

The Challenge of Containerizing Clean Architecture

In a standard single-project Web API, a Dockerfile simply copies the project files and runs dotnet publish. However, in a Clean Architecture solution, your API project depends on your Application and Infrastructure libraries, which in turn depend on your Domain library.

If your Docker build context is set incorrectly or if you copy files out of order, Docker's layer caching mechanism will break, or the build will fail due to missing project references.

Step 1: Writing an Optimized Multi-Stage Dockerfile

Place your Dockerfile at the solution root directory (right alongside your .sln file). A multi-stage build uses the heavy .NET SDK image to compile and publish the app, but copies the final output into a lightweight .NET ASP.NET runtime image for production. This drastically shrinks your final container size.

Dockerfile

# Stage 1: Build & Publish Environment
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy the solution file and individual project files to preserve directory structure for NuGet restores
COPY ["YourSolution.sln", "./"]
COPY ["src/YourSolution.Domain/YourSolution.Domain.csproj", "src/YourSolution.Domain/"]
COPY ["src/YourSolution.Application/YourSolution.Application.csproj", "src/YourSolution.Application/"]
COPY ["src/YourSolution.Infrastructure/YourSolution.Infrastructure.csproj", "src/YourSolution.Infrastructure/"]
COPY ["src/YourSolution.API/YourSolution.API.csproj", "src/YourSolution.API/"]
COPY ["tests/YourSolution.UnitTests/YourSolution.UnitTests.csproj", "tests/YourSolution.UnitTests/"]

# Restore NuGet packages across all projects
RUN dotnet restore "YourSolution.sln"

# Copy the remaining source code files
COPY . .

# Build and publish the API project in Release mode
WORKDIR "/src/src/YourSolution.API"
RUN dotnet publish "YourSolution.API.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Stage 2: Lean Runtime Environment
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

# Copy published artifacts from the build stage
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "YourSolution.API.dll"]

Step 2: Local Orchestration with Docker Compose

To test your containerized API locally alongside a backing database (like SQL Server or PostgreSQL), create a docker-compose.yml file at your solution root:

YAML

version: '3.8'

services:
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "YourStrongPassword123!"
    ports:
      - "1433:1433"
    networks:
      - app-net

  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5000:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings:DefaultConnection=Server=db;Database=EnterpriseDb;User Id=sa;Password=YourStrongPassword123!;TrustServerCertificate=True;
    depends_on:
      - db
    networks:
      - app-net

networks:
  app-net:
    driver: bridge

Run docker compose up --build in your terminal, and both your database and your Clean Architecture API will spin up and connect automatically.

Step 3: Automating CI/CD with GitHub Actions

Manual deployments are prone to human error. By setting up a GitHub Actions pipeline, every push to your main branch will automatically trigger a build, run your unit and integration test suites, build the Docker image, and push it to a container registry.

Create a workflow file at .github/workflows/ci-cd.yml:

YAML

name: CI/CD Pipeline

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Code
      uses: actions/checkout@v4

    - name: Setup .NET Core SDK
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '8.0.x'

    - name: Restore Dependencies
      run: dotnet restore

    - name: Build Solution
      run: dotnet build --no-restore --configuration Release

    - name: Run Unit & Integration Tests
      run: dotnet test --no-build --verbosity normal --configuration Release

  docker-build-and-push:
    needs: build-and-test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Code
      uses: actions/checkout@v4

    - name: Log in to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract Metadata for Docker
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ghcr.io/${{ github.repository }}/api

    - name: Build and Push Docker Image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}

Summary

Containerizing and automating a clean architecture solution guarantees environment parity between your development machine and your production servers. By structuring your Dockerfile around your solution file and letting GitHub Actions handle testing and image publishing, you ensure that only clean, verified code ever makes it to production.