Docker, Deployment, GitHub Actions & CI/CD Masterclass
Welcome to Phase 17 (Chapter 46): Docker, Deployment, GitHub Actions CI/CD & Health Checks Masterclass! Building an application is half the job. Deploying it reliably, consistently, and automatically is the other half. In this chapter, we master containerizing ASP.NET Core apps with Docker, writing Dockerfiles and docker-compose, setting up GitHub Actions CI/CD pipelines, configuring production environment variables, implementing Health Checks, and deploying to cloud platforms.
| Aspect | Virtual Machine (VM) | Docker Container |
|---|---|---|
| Size | Gigabytes (includes full OS) | Megabytes (shares host OS kernel) |
| Startup Time | Minutes (boots entire OS) | Seconds (process isolation) |
| Isolation Level | Complete hardware virtualization | Process-level namespace isolation |
| Portability | Hypervisor-dependent | Runs identically on any Docker host |
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy project file and restore dependencies
COPY ["ProductsAPI/ProductsAPI.csproj", "ProductsAPI/"]
RUN dotnet restore "ProductsAPI/ProductsAPI.csproj"
# Copy all source and build
COPY . .
WORKDIR "/src/ProductsAPI"
RUN dotnet build "ProductsAPI.csproj" -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "ProductsAPI.csproj" -c Release -o /app/publish
# Stage 3: Runtime (smallest possible final image)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductsAPI.dll"]
version: '3.9'
services:
api:
build: .
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__DefaultConnection=Server=db;Database=ProductsDB;User Id=sa;Password=YourPass123!
depends_on:
- db
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourPass123!
ports:
- "1433:1433"
volumes:
- sqldata:/var/opt/mssql
volumes:
sqldata:
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-test-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Run Tests
run: dotnet test --no-build -c Release --verbosity normal
- name: Build Docker Image
run: docker build -t productsapi:latest .
- name: Deploy to Azure App Service
uses: azure/webapps-deploy@v2
with:
app-name: 'my-productsapi'
publish-profile: DOLLAR{{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
images: 'productsapi:latest'
// Register health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>() // Checks DB connectivity
.AddUrlGroup(new Uri("https://api.github.com"), "GitHub"); // External dependency
// Map health endpoints
app.MapHealthChecks("/health"); // Simple: healthy/unhealthy
app.MapHealthChecks("/health/detailed", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}); // Detailed JSON report
Q1: Why use multi-stage Docker builds?
Multi-stage builds use the full .NET SDK (large image ~800MB) only during the build stage, then copy the compiled output to a minimal ASP.NET runtime image (~200MB) for the final container. This drastically reduces image size, attack surface, and deployment time.
Q2: How do I manage secrets (connection strings, API keys) in Docker?
Never hardcode secrets in Dockerfiles or docker-compose.yml committed to Git. Use environment variables injected at runtime, Docker Secrets, Kubernetes Secrets, or Azure Key Vault. Use GitHub Actions Secrets for CI/CD pipeline credentials.