Documents the complete staging setup including: - Hetzner VPS and Namecheap DNS configuration - Caddy reverse proxy with auto-SSL - SvelteKit runtime environment variable pattern - Docker Compose configuration - Domain mappings and URLs - Troubleshooting guide - Instructions for adding new apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
Staging Environment Setup Guide
This document describes the complete staging environment setup for ManaCore apps on Hetzner VPS with HTTPS via Caddy reverse proxy.
Overview
| Component | Details |
|---|---|
| Server | Hetzner VPS (46.224.108.214) |
| Domain | manacore.ai (Namecheap) |
| Reverse Proxy | Caddy (auto-SSL via Let's Encrypt) |
| Container Runtime | Docker Compose |
| SSH Access | ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 |
Architecture
┌─────────────────────────────────────────────┐
│ Hetzner VPS (46.224.108.214) │
│ │
Internet │ ┌─────────────────────────────────────┐ │
│ │ │ Caddy (ports 80/443) │ │
│ │ │ Auto-SSL via Let's Encrypt │ │
▼ │ └──────────────┬──────────────────────┘ │
┌──────────────┐ │ │ │
│ Namecheap │ │ ▼ │
│ DNS Records │────────────────────│ ┌─────────────────────────────────────┐ │
│ │ │ │ Docker Compose Services │ │
│ *.staging │ │ │ │ │
│ A → IP │ │ │ mana-core-auth:3001 │ │
└──────────────┘ │ │ chat-web:3000 / chat-backend:3002 │ │
│ │ clock-web:5187 / clock-backend:3017│ │
│ │ calendar-web:5186 / calendar-api:3016│ │
│ │ todo-web:5188 / todo-backend:3018 │ │
│ │ manacore-web:5173 │ │
│ │ postgres:5432 / redis:6379 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Domain Mapping
DNS Configuration (Namecheap)
| Type | Host | Value | TTL |
|---|---|---|---|
| A | staging |
46.224.108.214 | Automatic |
| A | *.staging |
46.224.108.214 | Automatic |
The wildcard record *.staging enables all subdomains like auth.staging.manacore.ai, clock.staging.manacore.ai, etc.
Staging URLs
| Service | URL | Internal Port |
|---|---|---|
| Auth | https://auth.staging.manacore.ai | 3001 |
| ManaCore Web | https://staging.manacore.ai | 5173 |
| Chat Web | https://chat.staging.manacore.ai | 3000 |
| Chat API | https://chat-api.staging.manacore.ai | 3002 |
| Clock Web | https://clock.staging.manacore.ai | 5187 |
| Clock API | https://clock-api.staging.manacore.ai | 3017 |
| Calendar Web | https://calendar.staging.manacore.ai | 5186 |
| Calendar API | https://calendar-api.staging.manacore.ai | 3016 |
| Todo Web | https://todo.staging.manacore.ai | 5188 |
| Todo API | https://todo-api.staging.manacore.ai | 3018 |
Caddy Reverse Proxy
Installation (One-time setup)
# SSH into server
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214
# Create Caddy data directory
mkdir -p ~/caddy_data ~/caddy_config
# Run Caddy container
docker run -d \
--name caddy \
--network host \
--restart unless-stopped \
-v ~/Caddyfile:/etc/caddy/Caddyfile \
-v ~/caddy_data:/data \
-v ~/caddy_config:/config \
caddy:2-alpine
Configuration
The Caddyfile is stored at:
- Server:
~/Caddyfile - Repo:
docker/caddy/Caddyfile.staging
# ManaCore Staging Reverse Proxy
auth.staging.manacore.ai {
reverse_proxy localhost:3001
}
chat.staging.manacore.ai {
reverse_proxy localhost:3000
}
chat-api.staging.manacore.ai {
reverse_proxy localhost:3002
}
staging.manacore.ai {
reverse_proxy localhost:5173
}
calendar.staging.manacore.ai {
reverse_proxy localhost:5186
}
calendar-api.staging.manacore.ai {
reverse_proxy localhost:3016
}
clock.staging.manacore.ai {
reverse_proxy localhost:5187
}
clock-api.staging.manacore.ai {
reverse_proxy localhost:3017
}
todo.staging.manacore.ai {
reverse_proxy localhost:5188
}
todo-api.staging.manacore.ai {
reverse_proxy localhost:3018
}
Updating Caddy Configuration
# Copy updated config to server
scp -i ~/.ssh/hetzner_deploy_key docker/caddy/Caddyfile.staging deploy@46.224.108.214:~/Caddyfile
# Reload Caddy (no downtime)
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 "docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
Caddy Management Commands
# View logs
docker logs caddy -f
# Restart Caddy
docker restart caddy
# Check Caddy status
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
SvelteKit Runtime Environment Variables
The Problem
SvelteKit's $env/static/public variables are replaced at build time. When Docker images are built in CI, the environment variables are baked into the JavaScript bundles. This means containers cannot use different URLs for different environments.
The Solution
Use $env/dynamic/private in hooks.server.ts to read environment variables at runtime, then inject them into the HTML for client-side access.
Implementation
Each SvelteKit web app has a hooks.server.ts that:
- Reads
_CLIENTenvironment variables at runtime - Injects them into the HTML via
<script>tag - Makes them available on
window.__PUBLIC_*__
Example: apps/clock/apps/web/src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
export const handle: Handle = async ({ event, resolve }) => {
// Read env vars at RUNTIME (not build time)
const authUrlClient = env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || env.PUBLIC_MANA_CORE_AUTH_URL || '';
const backendUrlClient = env.PUBLIC_BACKEND_URL_CLIENT || env.PUBLIC_BACKEND_URL || '';
return resolve(event, {
transformPageChunk: ({ html }) => {
// Inject into HTML for client-side access
const envScript = `<script>
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${authUrlClient}";
window.__PUBLIC_BACKEND_URL__ = "${backendUrlClient}";
</script>`;
return html.replace('<head>', `<head>${envScript}`);
},
});
};
Environment Variable Pattern
Each web app container receives two sets of URLs:
| Variable | Purpose | Example |
|---|---|---|
PUBLIC_BACKEND_URL |
Server-side (Docker network) | http://clock-backend:3017 |
PUBLIC_BACKEND_URL_CLIENT |
Client-side (browser) | https://clock-api.staging.manacore.ai |
PUBLIC_MANA_CORE_AUTH_URL |
Server-side auth | http://mana-core-auth:3001 |
PUBLIC_MANA_CORE_AUTH_URL_CLIENT |
Client-side auth | https://auth.staging.manacore.ai |
Docker Compose Configuration
File Locations
| File | Purpose |
|---|---|
docker-compose.staging.yml |
Staging configuration (repo) |
~/manacore-staging/docker-compose.yml |
Server deployment |
Key Configuration Sections
Web App Environment Variables:
clock-web:
environment:
NODE_ENV: staging
PORT: 5187
# Server-side URLs (Docker internal network)
PUBLIC_BACKEND_URL: http://clock-backend:3017
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
# Client-side URLs (browser access via HTTPS)
PUBLIC_BACKEND_URL_CLIENT: https://clock-api.staging.manacore.ai
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai
Backend CORS Configuration:
clock-backend:
environment:
CORS_ORIGINS: https://clock.staging.manacore.ai,https://staging.manacore.ai,http://localhost:5187
Auth Service CORS:
mana-core-auth:
environment:
CORS_ORIGINS: https://chat.staging.manacore.ai,https://staging.manacore.ai,https://calendar.staging.manacore.ai,https://clock.staging.manacore.ai,https://todo.staging.manacore.ai,http://localhost:3000,http://localhost:5173
Syncing Configuration to Server
# Copy docker-compose to server
scp -i ~/.ssh/hetzner_deploy_key docker-compose.staging.yml deploy@46.224.108.214:~/manacore-staging/docker-compose.yml
# Recreate containers with new config
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 "cd ~/manacore-staging && docker compose up -d --force-recreate"
Deployment Workflow
CI/CD Pipeline
The GitHub Actions workflow (.github/workflows/cd-staging.yml):
- Builds Docker images on push to
devbranch - Pushes images to GitHub Container Registry (ghcr.io)
- SSHs into staging server
- Pulls latest images
- Restarts containers
Manual Deployment
# 1. Build and push images (from local)
docker build -t ghcr.io/memo-2023/clock-web:latest -f apps/clock/apps/web/Dockerfile .
docker push ghcr.io/memo-2023/clock-web:latest
# 2. SSH into server
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214
# 3. Pull and restart
cd ~/manacore-staging
docker compose pull
docker compose up -d --force-recreate
Updating Environment Variables
- Edit
docker-compose.staging.ymllocally - Copy to server:
scp -i ~/.ssh/hetzner_deploy_key docker-compose.staging.yml deploy@46.224.108.214:~/manacore-staging/docker-compose.yml - Recreate affected containers:
docker compose up -d --force-recreate <service-name>
Troubleshooting
Mixed Content Errors
Symptom: Browser console shows "Mixed Content: The page was loaded over HTTPS, but requested an insecure resource"
Cause: Client-side JavaScript is calling HTTP URLs instead of HTTPS
Solution:
- Check
_CLIENTenvironment variables in docker-compose.yml - Ensure they use
https://staging domains - Recreate web containers:
docker compose up -d --force-recreate <web-service>
CORS Errors
Symptom: Browser console shows "Access-Control-Allow-Origin" errors
Cause: Backend CORS_ORIGINS doesn't include the HTTPS staging domain
Solution:
- Add the HTTPS domain to
CORS_ORIGINSin docker-compose.yml - Recreate backend containers
Caddy SSL Certificate Issues
Symptom: Browser shows SSL certificate warning
Solution:
# Check Caddy logs
docker logs caddy
# Force certificate renewal
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Container Health Check Failures
Symptom: Container shows "unhealthy" status
Solution:
# Check container logs
docker logs <container-name>
# Check health status
docker inspect <container-name> | grep -A 20 Health
Adding a New App to Staging
1. Update DNS (if needed)
If using a new subdomain pattern, update Namecheap DNS. The *.staging wildcard should cover most cases.
2. Update Caddyfile
Add entries for web and API:
newapp.staging.manacore.ai {
reverse_proxy localhost:<WEB_PORT>
}
newapp-api.staging.manacore.ai {
reverse_proxy localhost:<API_PORT>
}
3. Update docker-compose.staging.yml
Add the new services with proper environment variables:
newapp-web:
image: ghcr.io/memo-2023/newapp-web:latest
environment:
PUBLIC_BACKEND_URL: http://newapp-backend:<API_PORT>
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
PUBLIC_BACKEND_URL_CLIENT: https://newapp-api.staging.manacore.ai
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai
ports:
- "<WEB_PORT>:<WEB_PORT>"
4. Implement hooks.server.ts
Copy the runtime env var pattern from an existing app:
import type { Handle } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
export const handle: Handle = async ({ event, resolve }) => {
const authUrlClient = env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || '';
const backendUrlClient = env.PUBLIC_BACKEND_URL_CLIENT || '';
return resolve(event, {
transformPageChunk: ({ html }) => {
const envScript = `<script>
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${authUrlClient}";
window.__PUBLIC_BACKEND_URL__ = "${backendUrlClient}";
</script>`;
return html.replace('<head>', `<head>${envScript}`);
},
});
};
5. Deploy
- Sync Caddyfile:
scp ... Caddyfile.staging deploy@server:~/Caddyfile - Reload Caddy:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile - Sync docker-compose:
scp ... docker-compose.staging.yml deploy@server:~/manacore-staging/docker-compose.yml - Deploy containers:
docker compose up -d
Quick Reference Commands
# SSH into server
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214
# View all containers
docker ps
# View container logs
docker logs -f <container-name>
# Restart a container
docker restart <container-name>
# Recreate containers with new config
cd ~/manacore-staging && docker compose up -d --force-recreate
# Check Caddy SSL certificates
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
# Test HTTPS endpoint
curl -s https://auth.staging.manacore.ai/api/v1/health
# Check container env vars
docker exec <container-name> printenv | grep -E 'CLIENT|CORS'