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.

Join the conversation! Your thoughts help the community grow.