Add Docker support

Docker configuration:
- Dockerfile: Multi-stage build with non-root user, health checks
- Dockerfile.migrations: Runs EF Core migrations on startup
- docker-compose.yml: Full stack with SQL Server, Azurite, app
- .dockerignore: Optimized build context
- .env.example: Template for API keys

Application changes:
- Added /health endpoint with EF Core database check
- Conditional HTTPS redirect (disabled in containers)
- DOTNET_RUNNING_IN_CONTAINER environment detection

Usage:
  cp .env.example .env  # Add your API keys
  docker-compose up -d  # Start all services

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 12:37:00 +01:00
parent 89d1f7e33b
commit c6d52a38b2
6 changed files with 245 additions and 1 deletions

60
.dockerignore Normal file
View File

@@ -0,0 +1,60 @@
# Git
.git
.gitignore
.gitattributes
# Build outputs
**/bin/
**/obj/
**/out/
# IDE and editor files
.vs/
.vscode/
.idea/
*.user
*.suo
*.userosscache
*.sln.docstates
# Test results
**/TestResults/
**/coverage/
# NuGet
**/packages/
# Documentation
*.md
!README.md
# Docker files (don't need to copy these into the image)
docker-compose*.yml
Dockerfile*
.dockerignore
# Local settings (may contain secrets)
**/appsettings.Development.json
**/appsettings.Local.json
**/*.local.json
**/secrets.json
# Environment files
.env
.env.*
*.env
# Logs
**/logs/
**/*.log
# Temporary files
**/tmp/
**/temp/
# OS files
.DS_Store
Thumbs.db
# Tests (not needed in production image)
tests/

54
Dockerfile Normal file
View File

@@ -0,0 +1,54 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy solution and project files first for better layer caching
COPY TrueCV.sln ./
COPY src/TrueCV.Domain/TrueCV.Domain.csproj src/TrueCV.Domain/
COPY src/TrueCV.Application/TrueCV.Application.csproj src/TrueCV.Application/
COPY src/TrueCV.Infrastructure/TrueCV.Infrastructure.csproj src/TrueCV.Infrastructure/
COPY src/TrueCV.Web/TrueCV.Web.csproj src/TrueCV.Web/
# Restore dependencies
RUN dotnet restore
# Copy all source code
COPY src/ src/
# Build and publish
WORKDIR /src/src/TrueCV.Web
RUN dotnet publish -c Release -o /app/publish --no-restore
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create non-root user for security
RUN groupadd -r truecv && useradd -r -g truecv truecv
# Copy published app
COPY --from=build /app/publish .
# Set ownership
RUN chown -R truecv:truecv /app
# Switch to non-root user
USER truecv
# Expose port
EXPOSE 8080
# Set environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_RUNNING_IN_CONTAINER=true
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Start the app
ENTRYPOINT ["dotnet", "TrueCV.Web.dll"]

26
Dockerfile.migrations Normal file
View File

@@ -0,0 +1,26 @@
# Migrations runner
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Install EF Core tools
RUN dotnet tool install --global dotnet-ef
ENV PATH="$PATH:/root/.dotnet/tools"
# Copy solution and project files
COPY TrueCV.sln ./
COPY src/TrueCV.Domain/TrueCV.Domain.csproj src/TrueCV.Domain/
COPY src/TrueCV.Application/TrueCV.Application.csproj src/TrueCV.Application/
COPY src/TrueCV.Infrastructure/TrueCV.Infrastructure.csproj src/TrueCV.Infrastructure/
COPY src/TrueCV.Web/TrueCV.Web.csproj src/TrueCV.Web/
# Restore dependencies
RUN dotnet restore
# Copy all source code
COPY src/ src/
# Build the project
RUN dotnet build src/TrueCV.Web/TrueCV.Web.csproj -c Release
# Run migrations on startup
ENTRYPOINT ["dotnet", "ef", "database", "update", "--project", "src/TrueCV.Infrastructure", "--startup-project", "src/TrueCV.Web", "--no-build", "-c", "Release"]

88
docker-compose.yml Normal file
View File

@@ -0,0 +1,88 @@
version: '3.8'
services:
# TrueCV Web Application
truecv-web:
build:
context: .
dockerfile: Dockerfile
container_name: truecv-web
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TrueCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
- ConnectionStrings__HangfireConnection=Server=sqlserver;Database=TrueCV_Hangfire;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
- AzureBlob__ConnectionString=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;
- AzureBlob__ContainerName=cv-uploads
- CompaniesHouse__BaseUrl=https://api.company-information.service.gov.uk
- CompaniesHouse__ApiKey=${COMPANIES_HOUSE_API_KEY:-}
- Anthropic__ApiKey=${ANTHROPIC_API_KEY:-}
depends_on:
sqlserver:
condition: service_healthy
azurite:
condition: service_started
networks:
- truecv-network
restart: unless-stopped
# SQL Server Database
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: truecv-sqlserver
ports:
- "1433:1433"
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=TrueCV_P@ssw0rd!
- MSSQL_PID=Developer
volumes:
- sqlserver-data:/var/opt/mssql
networks:
- truecv-network
healthcheck:
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "TrueCV_P@ssw0rd!" -C -Q "SELECT 1" || exit 1
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
# Azure Storage Emulator (Azurite)
azurite:
image: mcr.microsoft.com/azure-storage/azurite:latest
container_name: truecv-azurite
ports:
- "10000:10000" # Blob service
- "10001:10001" # Queue service
- "10002:10002" # Table service
volumes:
- azurite-data:/data
command: "azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --location /data --debug /data/debug.log"
networks:
- truecv-network
restart: unless-stopped
# Database initialization (runs migrations)
db-init:
build:
context: .
dockerfile: Dockerfile.migrations
container_name: truecv-db-init
environment:
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TrueCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
depends_on:
sqlserver:
condition: service_healthy
networks:
- truecv-network
restart: "no"
networks:
truecv-network:
driver: bridge
volumes:
sqlserver-data:
azurite-data:

View File

@@ -56,6 +56,10 @@ try
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
// Add health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>("database");
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -66,7 +70,15 @@ try
app.UseHsts();
}
app.UseHttpsRedirection();
// Only use HTTPS redirection when not running in Docker/container
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")))
{
// Running in container - skip HTTPS redirect (handled by reverse proxy)
}
else
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseAntiforgery();
@@ -93,6 +105,9 @@ try
return Results.Redirect("/");
}).RequireAuthorization();
// Health check endpoint
app.MapHealthChecks("/health");
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();

View File

@@ -10,6 +10,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.*" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
</ItemGroup>