Deployment¶
Environments¶
| Environment | URL | Description |
|---|---|---|
| Development | localhost | Local dev via Aspire |
| Production | VPS Hostinger | Docker Compose via Aspire CLI |
Local Development (Aspire)¶
Aspire automatically:
- Starts infrastructure containers (PostgreSQL)
- Builds and runs APIs
- Runs frontend dev server
- Runs documentation
Note: Keycloak must be started separately from the
Keycloakrepo (docker compose up -d) before running the Recron AppHost. The AppHost connects to Keycloak via thekeycloak-authorityparameter (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 (
Traefikrepo) runs once on the VPS, auto-discovers Docker containers via labels - Keycloak (
Keycloakrepo) 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¶
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:
- Traefik: Clone the
Traefikrepo to/opt/traefik/, configure.env, rundocker compose up -d - Keycloak: Clone the
Keycloakrepo to/opt/keycloak/, configure.env, rundocker 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 variables → Actions (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¶
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Set expiration (recommended: 90 days)
- Select scopes:
- [x]
read:packages - [x]
write:packages - Click Generate token
- Copy the token immediately (you won't see it again)
- Add as
GHCR_TOKENsecret in your repository
Generating Secure Passwords¶
Complete Secrets Checklist¶
Before running deployment, verify all secrets are configured:
- [ ]
VPS_HOST- VPS IP address - [ ]
VPS_USER- SSH username (usuallyroot) - [ ]
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¶
- Log in to hpanel.hostinger.com
- Go to Domains → select your domain → DNS / Nameservers or DNS Zone
- Find the existing A record for
@and edit it to point to your VPS IP - Add new A records for
auth,api,app,docs,velvet - 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:
- Browser resolves
auth.bluebraces.online→31.97.182.130 - Browser connects to
31.97.182.130:443with headerHost: auth.bluebraces.online - Traefik matches the
Host()rule on the Keycloak container and proxies the request - 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:
- Build and test backend + frontend
- Publish Docker images to GHCR
- Generate
docker-compose.yamlwith Aspire CLI - Deploy to VPS with
docker-compose.traefik.ymloverlay - 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¶
- Check that Traefik and Keycloak are running (deployed from their own repos)
- Verify Keycloak is accessible at
https://auth.bluebraces.online/ - Run the Recron deployment pipeline (push to
main) - 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¶
- Ensures the
proxyDocker network exists (shared with Traefik) docker compose -f docker-compose.yaml -f docker-compose.traefik.yml pull— pulls updated imagesdocker compose -f docker-compose.yaml -f docker-compose.traefik.yml up -d --remove-orphans— starts all services with Traefik labels- 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:
- Create
/opt/your-project/on the VPS - Add a
docker-compose.ymlwith 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 - Add DNS A record for
your-apppointing to the VPS IP - Run
docker compose up -d— Traefik auto-discovers and generates SSL