Skip to content

Deployment

Environments

Environment URL Description
Development localhost Local dev via Aspire
Production VPS Hostinger Docker Compose via Aspire CLI

Local Development (Aspire)

cd Recron.AppHost
dotnet run

Aspire automatically:

  • Starts infrastructure containers (PostgreSQL)
  • Builds and runs APIs
  • Runs frontend dev server
  • Runs documentation

Note: Keycloak must be started separately from the Keycloak repo (docker compose up -d) before running the Recron AppHost. The AppHost connects to Keycloak via the keycloak-authority parameter (default: https://auth.bluebraces.online).

Production Deployment (VPS)

Architecture

The VPS uses Traefik as a shared reverse proxy for all projects (Recron, QuantCore, etc.). Traefik and Keycloak are shared infrastructure deployed from their own standalone repositories.

  • Traefik (Traefik repo) runs once on the VPS, auto-discovers Docker containers via labels
  • Keycloak (Keycloak repo) runs once on the VPS as a shared identity provider
  • SSL certificates are generated and renewed automatically by Traefik via Let's Encrypt
  • Each project deploys independently and registers itself with Traefik through Docker labels
  • No manual nginx config, no certbot scripts, no certificate management
VPS (/opt)
├── traefik/                ← shared reverse proxy (Traefik repo)
│   ├── docker-compose.yml
│   ├── .env                ← ACME_EMAIL
│   └── acme.json           ← auto-generated SSL certificates
├── keycloak/               ← shared identity provider (Keycloak repo)
│   ├── docker-compose.yml
│   ├── .env                ← admin credentials, DB credentials
│   ├── realms/
│   │   └── realm-export.json
│   └── themes/
│       └── keywind/
├── recron/                 ← this project
│   ├── docker-compose.yaml ← generated by Aspire
│   ├── docker-compose.traefik.yml ← Traefik routing labels
│   └── .env
└── velvet/                 ← VelvetUi demo (separate repo/pipeline)
    ├── docker-compose.yml
    └── .env

Aspire CLI generates docker-compose.yaml with all services. The docker-compose.traefik.yml overlay adds Traefik labels for routing and SSL.

Requirements

  • VPS with Ubuntu 22.04/24.04 (minimum 4GB RAM, recommended 8GB)
  • GitHub account with repository access
  • SSH key for VPS

Step 1: Prepare VPS

1.1 Connect to VPS

ssh root@YOUR_VPS_IP

1.2 Install prerequisites

Install Docker and Docker Compose, then set up security:

# Install Docker (official script)
curl -fsSL https://get.docker.com | sh

# Install Docker Compose plugin
apt-get install -y docker-compose-plugin

# Configure firewall
ufw allow 22/tcp   # SSH
ufw allow 80/tcp   # HTTP
ufw allow 443/tcp  # HTTPS
ufw --force enable

# Install fail2ban for SSH protection
apt-get install -y fail2ban
systemctl enable fail2ban

1.3 Deploy shared infrastructure

Before deploying Recron, set up Traefik and Keycloak from their standalone repos. See the README in each repo for instructions:

  1. Traefik: Clone the Traefik repo to /opt/traefik/, configure .env, run docker compose up -d
  2. Keycloak: Clone the Keycloak repo to /opt/keycloak/, configure .env, run docker compose up -d

This is a one-time setup. Both services persist their data across restarts.

Step 2: Configure GitHub Secrets

Go to your repository Settings → Secrets and variables → Actions → New repository secret.

!!! info "How to add a secret" 1. Navigate to your GitHub repository 2. Click Settings (top menu) 3. Click Secrets and variablesActions (left sidebar) 4. Click New repository secret 5. Enter the Name and Secret value 6. Click Add secret

VPS/SSH Secrets (required for deployment)

Secret Description Example Value
VPS_HOST VPS IP address 31.97.182.130
VPS_USER SSH username root
VPS_SSH_KEY Private SSH key (see Step 3) -----BEGIN OPENSSH PRIVATE KEY-----...
GHCR_TOKEN GitHub PAT for container registry ghp_xxxxxxxxxxxx

Application Secrets (required for deployment)

Secret Description Recommended Value
DB_USERNAME PostgreSQL username recron
DB_PASSWORD PostgreSQL password Generate: openssl rand -base64 32
SCALAR_CLIENT_ID Scalar API OAuth client ID scalar
SCALAR_CLIENT_SECRET Scalar API OAuth client secret Generate: openssl rand -base64 32
OPENAI_API_KEY OpenAI API key sk-proj-... (from OpenAI dashboard)
GOOGLE_CLIENT_ID Google OAuth client ID (Google Calendar) From Google Cloud Console
GOOGLE_CLIENT_SECRET Google OAuth client secret From Google Cloud Console
TOKEN_ENCRYPTION_KEY AES key for encrypting Google OAuth tokens Generate: openssl rand -base64 32

!!! warning "Security" - Never commit secrets to the repository - Use strong, unique passwords (minimum 32 characters) - Rotate secrets periodically - Remove API keys from appsettings.json - use secrets only

Creating GHCR_TOKEN

  1. Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Click Generate new token (classic)
  3. Set expiration (recommended: 90 days)
  4. Select scopes:
  5. [x] read:packages
  6. [x] write:packages
  7. Click Generate token
  8. Copy the token immediately (you won't see it again)
  9. Add as GHCR_TOKEN secret in your repository

Generating Secure Passwords

# Generate a random 32-character password
[System.Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(24))
# Generate a random 32-character password
openssl rand -base64 32

Complete Secrets Checklist

Before running deployment, verify all secrets are configured:

  • [ ] VPS_HOST - VPS IP address
  • [ ] VPS_USER - SSH username (usually root)
  • [ ] VPS_SSH_KEY - Private SSH key
  • [ ] GHCR_TOKEN - GitHub Personal Access Token
  • [ ] DB_USERNAME - Database username
  • [ ] DB_PASSWORD - Database password
  • [ ] SCALAR_CLIENT_ID - Scalar client ID
  • [ ] SCALAR_CLIENT_SECRET - Scalar client secret
  • [ ] OPENAI_API_KEY - OpenAI API key
  • [ ] GOOGLE_CLIENT_ID - Google OAuth client ID
  • [ ] GOOGLE_CLIENT_SECRET - Google OAuth client secret
  • [ ] TOKEN_ENCRYPTION_KEY - AES encryption key for OAuth tokens

Step 3: Generate SSH Key

# Create .ssh directory if it doesn't exist
mkdir ~/.ssh -ErrorAction SilentlyContinue

# Generate Ed25519 key (press Enter twice for empty passphrase)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f C:\Users\YOUR_USERNAME\.ssh\recron_deploy

# Add VPS to known hosts (type 'yes' when prompted)
ssh root@YOUR_VPS_IP

# Copy public key to VPS (enter VPS password when prompted)
Get-Content C:\Users\YOUR_USERNAME\.ssh\recron_deploy.pub | ssh root@YOUR_VPS_IP "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

# Test connection (should connect without password)
ssh -i C:\Users\YOUR_USERNAME\.ssh\recron_deploy root@YOUR_VPS_IP

# Display private key to copy to GitHub Secrets
Get-Content C:\Users\YOUR_USERNAME\.ssh\recron_deploy
# Generate Ed25519 key with empty passphrase
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/recron_deploy -N ""

# Copy public key to VPS
ssh-copy-id -i ~/.ssh/recron_deploy.pub root@YOUR_VPS_IP

# Test connection (should connect without password)
ssh -i ~/.ssh/recron_deploy root@YOUR_VPS_IP

# Display private key to copy to GitHub Secrets
cat ~/.ssh/recron_deploy

Important

Copy the entire private key content, including the header and footer lines: -----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----

Step 4: Configure DNS

In your domain registrar's DNS Zone (e.g., Hostinger), add A records pointing to your VPS IP.

Important: All records must point to the same VPS IP address.

Required DNS Records

Type Name Points to TTL
A @ YOUR_VPS_IP 14400
A auth YOUR_VPS_IP 14400
A api YOUR_VPS_IP 14400
A app YOUR_VPS_IP 14400
A docs YOUR_VPS_IP 14400
A velvet YOUR_VPS_IP 14400
Hostinger-Specific Instructions
  1. Log in to hpanel.hostinger.com
  2. Go to Domains → select your domain → DNS / Nameservers or DNS Zone
  3. Find the existing A record for @ and edit it to point to your VPS IP
  4. Add new A records for auth, api, app, docs, velvet
  5. In the "Name" field, enter only the subdomain name (e.g., auth), not the full domain

How routing works with Traefik

All subdomains point to the same IP address. Traefik (reverse proxy) routes requests to different containers based on the Host header and Docker labels.

Example flow for https://auth.bluebraces.online:

  1. Browser resolves auth.bluebraces.online31.97.182.130
  2. Browser connects to 31.97.182.130:443 with header Host: auth.bluebraces.online
  3. Traefik matches the Host() rule on the Keycloak container and proxies the request
  4. SSL certificate was auto-generated by Traefik on the first request

DNS Propagation

DNS changes can take up to 24 hours to propagate globally (usually a few minutes).

Verify domains resolve correctly:

nslookup bluebraces.online
nslookup auth.bluebraces.online
nslookup api.bluebraces.online
nslookup app.bluebraces.online
nslookup docs.bluebraces.online

Each command should return your VPS IP address.

Step 5: Deploy

Automatic (CI/CD)

# Push to main branch triggers deployment
git add .
git commit -m "feat: deploy configuration"
git push origin main

The pipeline will:

  1. Build and test backend + frontend
  2. Publish Docker images to GHCR
  3. Generate docker-compose.yaml with Aspire CLI
  4. Deploy to VPS with docker-compose.traefik.yml overlay
  5. Traefik automatically generates SSL certificates on first request

No manual SSL setup needed

Unlike the old nginx/certbot approach, Traefik handles SSL certificates fully automatically. Just push to main and certificates will be generated when DNS is properly configured.

Manual (local testing)

# Install Aspire CLI
dotnet tool install -g aspire.cli --prerelease

# Generate Docker Compose
cd Recron.AppHost
aspire publish --publisher docker-compose --output-path ../publish

# Check generated files
ls ../publish/
# docker-compose.yaml
# .env

Step 6: Verify Deployment

  1. Check that Traefik and Keycloak are running (deployed from their own repos)
  2. Verify Keycloak is accessible at https://auth.bluebraces.online/
  3. Run the Recron deployment pipeline (push to main)
  4. Verify all Recron services are accessible (see Endpoints below)

CI/CD Pipeline

flowchart TD
    A[Push to main] --> B[build-backend]
    A --> C[build-frontend]
    B --> D[publish]
    C --> D
    D --> E[Build Docker images]
    E --> F[Push to GHCR]
    F --> G[deploy]
    G --> H[SCP compose + traefik overlay to VPS]
    H --> I[docker compose -f ... -f traefik up -d]
    I --> J[Traefik auto-generates SSL certs]

Deploy Step Details

The deploy job performs these operations on the VPS:

1. File Transfer (SCP)

Files are copied from the CI runner to /opt/recron/ on the VPS:

Source (CI runner) Destination (VPS)
publish/docker-compose.yaml /opt/recron/docker-compose.yaml
deploy/docker-compose.traefik.yml /opt/recron/docker-compose.traefik.yml

2. Environment Configuration

The deploy script creates /opt/recron/.env with image tags, database credentials, and Keycloak URLs. This file is the single source of truth for all service configuration.

3. Service Deployment

  1. Ensures the proxy Docker network exists (shared with Traefik)
  2. docker compose -f docker-compose.yaml -f docker-compose.traefik.yml pull — pulls updated images
  3. docker compose -f docker-compose.yaml -f docker-compose.traefik.yml up -d --remove-orphans — starts all services with Traefik labels
  4. Traefik auto-discovers containers and generates SSL certificates

Endpoints After Deployment

With Custom Domain (SSL via Traefik)

Service URL
Main (redirect) https://bluebraces.online/
Webapp https://app.bluebraces.online/
Keycloak https://auth.bluebraces.online/
API - Questions https://api.bluebraces.online/questions/
API - Organizer https://api.bluebraces.online/organizer/
API - Users https://api.bluebraces.online/users/
Documentation https://docs.bluebraces.online/
Velvet Demo https://velvet.bluebraces.online/

Without Custom Domain (IP only)

Service URL
Webapp http://YOUR_VPS_IP:3000/
Gateway (API) http://YOUR_VPS_IP:5000/
Keycloak http://YOUR_VPS_IP:8080/
Documentation http://YOUR_VPS_IP:8081/

Monitoring

Check container status

ssh root@YOUR_VPS_IP "cd /opt/recron && docker compose -f docker-compose.yaml -f docker-compose.traefik.yml ps"

View logs

# All logs
ssh root@YOUR_VPS_IP "cd /opt/recron && docker compose logs -f"

# Specific service
ssh root@YOUR_VPS_IP "cd /opt/recron && docker compose logs -f organizer-api"

# Traefik logs (from Traefik repo)
ssh root@YOUR_VPS_IP "cd /opt/traefik && docker compose logs --tail 50"

Restart services

ssh root@YOUR_VPS_IP "cd /opt/recron && docker compose -f docker-compose.yaml -f docker-compose.traefik.yml restart"

Health Checks

Each API exposes a /health endpoint:

curl http://localhost:5000/organizer/health
curl http://localhost:5000/questions/health
curl http://localhost:5000/users/health

Rollback

# Change image tag to previous SHA
ssh root@YOUR_VPS_IP "cd /opt/recron && \
  sed -i 's/:current_sha/:previous_sha/g' docker-compose.yaml && \
  docker compose -f docker-compose.yaml -f docker-compose.traefik.yml up -d"

Structure After Deployment

/opt/
├── traefik/                       # Shared reverse proxy (Traefik repo)
│   ├── docker-compose.yml
│   ├── .env                       # ACME_EMAIL
│   └── acme.json                  # Auto-generated SSL certificates
├── keycloak/                      # Shared identity provider (Keycloak repo)
│   ├── docker-compose.yml
│   ├── .env                       # Admin + DB credentials
│   ├── realms/
│   │   └── realm-export.json
│   └── themes/
│       └── keywind/
├── recron/
│   ├── docker-compose.yaml        # Generated by Aspire
│   ├── docker-compose.traefik.yml # Traefik routing overlay
│   └── .env                       # Parameters + secrets
└── velvet/                        # VelvetUi demo (separate repo)
    ├── docker-compose.yml
    └── .env

Adding a New Project to the VPS

To deploy another project alongside Recron:

  1. Create /opt/your-project/ on the VPS
  2. Add a docker-compose.yml with Traefik labels:
    services:
      your-app:
        image: your-image:tag
        labels:
          - traefik.enable=true
          - traefik.http.routers.your-app.rule=Host(`your-app.bluebraces.online`)
          - traefik.http.routers.your-app.entrypoints=websecure
          - traefik.http.routers.your-app.tls.certresolver=letsencrypt
          - traefik.http.services.your-app.loadbalancer.server.port=3000
        networks:
          - proxy
    
    networks:
      proxy:
        external: true
    
  3. Add DNS A record for your-app pointing to the VPS IP
  4. Run docker compose up -d — Traefik auto-discovers and generates SSL