Deployment
Cpnucleo uses Docker Compose for containerized deployment and GitHub Actions for CI/CD, with final deployment to Hostinger Docker Manager.
Docker Compose Configurations
The project provides three compose configurations that can be layered:
Base (compose.yaml)
The base configuration defines all services with pre-built GHCR images:
| Service | Image | Internal Port | External Port |
|---|---|---|---|
| webapi1-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-api:latest | 5000 | 5100 |
| webapi2-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-api:latest | 5000 | 5111 |
| identityapi-cpnucleo | ghcr.io/jonathanperis/cpnucleo-identity-api:latest | 5010 | 5200 |
| grpcserver-cpnucleo | ghcr.io/jonathanperis/cpnucleo-grpc-server:latest | 5020/5021 | 5300/5301 |
| webclient-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-client:latest | 5030 | 5400 |
| db | postgres:16.7 | 5432 | 5432 |
| nginx | nginx | 9999 | 9999 |
All API services depend on db being healthy before starting.
Development Override (compose.override.yaml)
docker compose -f compose.yaml -f compose.override.yaml up --build
Differences from base:
- Builds from source using Dockerfiles in
src/ - Build args:
AOT=false,TRIM=false,EXTRA_OPTIMIZE=false,BUILD_CONFIGURATION=Debug - Adds Grafana LGTM OpenTelemetry stack (ports 3000, 4317, 4318)
- Resource limits: 0.4 CPU, 100MB memory per service
Production Override (compose.prod.yaml)
docker compose -f compose.yaml -f compose.prod.yaml up -d
Differences from base:
restart: alwayson all services- Resource reservations: 0.25 CPU / 256MB per API, 0.50 CPU / 512MB per DB
- Resource limits: 0.50 CPU / 512MB per API, 1.0 CPU / 1GB for DB
- JSON logging with rotation: 10MB max size, 3 files retained
- No build step; production image variables such as
CPNUCLEO_WEB_API_IMAGEare required and should point at immutable GHCR tags (for examplesha-...)
Dockerfiles
Each service has a multi-stage Dockerfile supporting configurable build options:
Build Arguments
| Argument | Description | Dev Value | Prod Value |
|---|---|---|---|
AOT |
Enable Native AOT compilation | false | false |
TRIM |
Enable assembly trimming with ReadyToRun | false | true |
EXTRA_OPTIMIZE |
Aggressive optimizations (remove symbols, disable debugger, invariant globalization) | false | true |
BUILD_CONFIGURATION |
.NET build configuration | Debug | Release |
ASPNETCORE_ENVIRONMENT |
Runtime environment | Development | Production |
DB_CONNECTION_STRING |
Database connection string | (from .env) | (from secrets) |
Build Stages
- base –
mcr.microsoft.com/dotnet/aspnet:10.0runtime image - build –
mcr.microsoft.com/dotnet/sdk:10.0with clang/zlib for AOT support; restores, builds - publish – Publishes with configured optimizations
- final – Copies published output to runtime image
Platform Support
The release workflow builds linux/amd64 and linux/arm64/v8 images, then merges them into latest and immutable sha-${GITHUB_SHA} manifests.
NGINX Reverse Proxy
NGINX load-balances traffic across two WebApi instances:
upstream api {
least_conn;
server webapi1-cpnucleo:5000;
server webapi2-cpnucleo:5000;
}
server {
listen 9999;
location / {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Configuration Highlights
- least_conn load balancing – sends requests to the server with fewest active connections
- gzip compression – level 5, minimum 256 bytes
- keepalive_timeout: 0 – persistent connections disabled for stateless APIs
- server_tokens: off – hides NGINX version
- access_log: off – disabled for performance
- epoll event model with multi_accept
GitHub Actions CI/CD
Build Check (build-check.yml)
Triggered on pull requests to main.
Jobs:
-
Setup, Build & Test (matrix: WebApi, GrpcServer, IdentityApi, WebClient)
- Checkout repository
- Setup .NET SDK (from global.json)
- Restore dependencies
- Build application (Debug, no AOT/Trim)
- Run Architecture Tests
-
Container Healthcheck Test (depends on build)
- Build Docker image from source
- Start container via Docker Compose
- Poll
/healthzendpoint up to 20 times with 5-second intervals - Fail if health check does not return 200
Main Release (main-release.yml)
Triggered on push to main and manual dispatch.
Jobs:
-
Setup, Build & Test – Same as build check but with
TRIM=true,EXTRA_OPTIMIZE=true,BUILD_CONFIGURATION=Release -
Build & Push Docker Images (depends on test)
- Build
linux/amd64images taggedsha-${GITHUB_SHA}-amd64andlatest - Build
linux/arm64/v8images taggedsha-${GITHUB_SHA}-arm64andlatest-arm64 - Merge both architectures into multi-arch
sha-${GITHUB_SHA}andlatestmanifests for each GHCR image
- Build
-
Container Healthcheck Test (depends on push)
- Pull production images
- Run healthcheck validation
-
Deploy to Hostinger Docker Manager (depends on amd64 images + container health checks)
- Deploy the production Compose project through
scripts/deploy-hostinger-docker-manager.sh - Uses Hostinger project secrets plus immutable
sha-${GITHUB_SHA}-amd64GHCR image tags - Verifies the public WebClient, WebApi, IdentityApi, and gRPC health routes after deployment
- Deploy the production Compose project through
Hostinger Deployment Targets
| Surface | Public URL | Backing service |
|---|---|---|
| WebClient | https://cpnucleo.jonathanperis.tech/ |
webclient-cpnucleo |
| WebApi | https://api-cpnucleo.jonathanperis.tech/ |
webapi1-cpnucleo / webapi2-cpnucleo |
| IdentityApi | https://identity-cpnucleo.jonathanperis.tech/ |
identityapi-cpnucleo |
| gRPC health | https://grpc-cpnucleo.jonathanperis.tech/healthz |
grpcserver-cpnucleo |
Environment Variables
Required (.env)
| Variable | Description | Example |
|---|---|---|
POSTGRES_USER |
PostgreSQL username | postgres |
POSTGRES_PASSWORD |
PostgreSQL password | postgres |
POSTGRES_DB |
Database name | cpnucleo |
DB_CONNECTION_STRING |
Full Npgsql connection string | Host=db;Username=postgres;… |
OTEL_EXPORTER_OTLP_ENDPOINT |
OpenTelemetry collector endpoint | http://otel-lgtm:4317 |
OTEL_METRIC_EXPORT_INTERVAL |
Metric export interval (ms) | 5000 |
GitHub Secrets (for CI/CD)
| Secret | Purpose |
|---|---|
GITHUB_TOKEN |
GHCR authentication (automatic) |
DB_CONNECTION_STRING |
Production database connection |
HOSTINGER_API_TOKEN |
Hostinger API authentication |
HOSTINGER_VPS_ID |
Target Hostinger VPS identifier |
HOSTINGER_PROJECT_NAME |
Docker Manager project name |
HOSTINGER_ENV_BASE64 |
Base64-encoded production .env payload |
CPNUCLEO_WEB_URL |
Public WebClient smoke-test URL |
CPNUCLEO_API_URL |
Public WebApi smoke-test URL |
CPNUCLEO_IDENTITY_URL |
Public IdentityApi smoke-test URL |
CPNUCLEO_GRPC_HEALTH_URL |
Public gRPC health smoke-test URL |
Network
All services communicate over a shared Docker bridge network:
networks:
default:
name: cpnucleo_network
driver: bridge
Service discovery uses Docker DNS (e.g., db, webapi1-cpnucleo).