From c6d52a38b294c0541b75541616fac2e5797dd21d Mon Sep 17 00:00:00 2001 From: peter Date: Mon, 19 Jan 2026 12:37:00 +0100 Subject: [PATCH] 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 --- .dockerignore | 60 ++++++++++++++++++++++ Dockerfile | 54 ++++++++++++++++++++ Dockerfile.migrations | 26 ++++++++++ docker-compose.yml | 88 ++++++++++++++++++++++++++++++++ src/TrueCV.Web/Program.cs | 17 +++++- src/TrueCV.Web/TrueCV.Web.csproj | 1 + 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.migrations create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f808e0d --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7866f57 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Dockerfile.migrations b/Dockerfile.migrations new file mode 100644 index 0000000..4b24b76 --- /dev/null +++ b/Dockerfile.migrations @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..97b31f5 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/src/TrueCV.Web/Program.cs b/src/TrueCV.Web/Program.cs index 9dceef4..2dfe1bd 100644 --- a/src/TrueCV.Web/Program.cs +++ b/src/TrueCV.Web/Program.cs @@ -56,6 +56,10 @@ try builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped>(); + // Add health checks + builder.Services.AddHealthChecks() + .AddDbContextCheck("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() .AddInteractiveServerRenderMode(); diff --git a/src/TrueCV.Web/TrueCV.Web.csproj b/src/TrueCV.Web/TrueCV.Web.csproj index 3ff754f..4399483 100644 --- a/src/TrueCV.Web/TrueCV.Web.csproj +++ b/src/TrueCV.Web/TrueCV.Web.csproj @@ -10,6 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +