calServer v2 — Installation Guide¶
Prerequisites¶
- Docker >= 24.0 and Docker Compose >= 2.20
- calServer v1 is already running (docker-compose.yml)
- Domain points to the server via DNS (A record)
- Ports 80 and 443 open (for Let's Encrypt HTTP-01 challenge)
- At least 1 GB additional RAM (API v2 ~200 MB, frontend ~100 MB, Redis ~50 MB)
Quick Start (Demo/Development Server)¶
1. Configure .env¶
Add the following variables to the existing .env:
# Enable v2 services
ENABLE_V2=true
# Generate Laravel app key (one-time):
LARAVEL_APP_KEY=base64:$(docker run --rm php:8.3-cli php -r "echo base64_encode(random_bytes(32));")
# Environment
APP_ENV=production
APP_DEBUG=false
APP_URL=https://develop.net-cal.com # Same domain as v1!
DB_PREFIX= # table prefix of the V1 database, if any
Important:
APP_URLmust match theDOMAINvariable. v2 does not run on a separate domain but under/v2/and/api/v2/.
2. Generate Laravel App Key¶
# Option A: Via Docker
docker run --rm php:8.3-cli php -r "echo 'base64:' . base64_encode(random_bytes(32)) . PHP_EOL;"
# Option B: If the Laravel container is already running
docker exec calserver-api-v2 php artisan key:generate --show
Enter the generated key in .env as LARAVEL_APP_KEY.
3. Pull Docker Images¶
# Manually (without CI/CD)
docker pull calhelp/calserver-api-v2:release-latest
docker pull calhelp/calserver-frontend:release-latest
# Or a specific tag
docker pull calhelp/calserver-api-v2:develop-latest
docker pull calhelp/calserver-frontend:develop-latest
4. Start Deployment¶
The script automatically detects ENABLE_V2=true and:
1. Appends docker-compose.v2.yml as an overlay
2. Starts the v2 services (API, frontend, Redis)
3. Runs health checks for all services
5. Verify¶
# v1 (unchanged)
curl -s https://develop.net-cal.com/ | head -5
# v2 API health check
curl -s https://develop.net-cal.com/api/v2/health
# → {"status":"ok"}
# v2 Frontend
curl -s -o /dev/null -w "%{http_code}" https://develop.net-cal.com/v2/
# → 200
# Container status
docker ps --filter name=calserver --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
Expected output:
NAMES STATUS PORTS
calserver Up 2 minutes 0.0.0.0:8080->8080/tcp
calserver-api-v2 Up 2 minutes 8081/tcp
calserver-frontend Up 2 minutes 3000/tcp
calserver-redis Up 2 minutes 6379/tcp
Let's Encrypt SSL¶
How It Works¶
v2 uses the same SSL certificate as v1:
Internet
│
├── https://develop.net-cal.com/ → calserver (v1, Yii)
├── https://develop.net-cal.com/api/v2/ → calserver-api-v2 (Laravel)
└── https://develop.net-cal.com/v2/ → calserver-frontend (Nuxt)
│
▼
┌─────────────────────────────────────────────┐
│ nginx-proxy (nginxproxy/nginx-proxy) │
│ - SSL termination via Let's Encrypt │
│ - Certificate: *.net-cal.com │
│ - vhost.d/default_location: │
│ /api/v2/* → calserver-api-v2:8081 │
│ /v2/* → calserver-frontend:3000 │
│ /* → calserver:8080 (default) │
└─────────────────────────────────────────────┘
No Additional Certificate Needed¶
The nginx-proxy container with acme-companion:
1. Detects LETSENCRYPT_HOST and VIRTUAL_HOST from the calserver container
2. Requests a certificate from Let's Encrypt (HTTP-01 challenge)
3. Stores it under /etc/nginx/certs/
4. Automatically configures HTTPS for the VIRTUAL_HOST
5. The vhost.d/default_location file is automatically included in the SSL server block
6. Result: /api/v2/* and /v2/* are immediately reachable via HTTPS
Check Configuration¶
# Check certificate
docker exec nginx-proxy ls -la /etc/nginx/certs/ | grep net-cal
# Check vhost.d location
docker exec nginx-proxy cat /etc/nginx/vhost.d/default_location
# Test Nginx config
docker exec nginx-proxy nginx -T 2>&1 | grep -A5 "location /api/v2"
Let's Encrypt Staging (Test Server)¶
On test servers, the Let's Encrypt staging environment should be used to avoid hitting the rate limit (5 certificates per week per domain).
Set in .env:
The acme-companion then uses the staging server (acme-staging-v02.api.letsencrypt.org).
Staging certificates are shown as "not trusted" in the browser
(Issuer: "Fake LE Intermediate X1"), but the SSL chain works correctly.
For production servers, omit the variable or leave it empty:
Let's Encrypt Renewal¶
Renewal happens automatically via the acme-companion container.
If the calserver container has LETSENCRYPT_RESTART_CONTAINER=true, nginx-proxy
is also reloaded upon renewal.
Building Manually (Without CI/CD)¶
If images should be built locally:
# From the repository root:
# API v2 image
docker build -f Dockerfile.laravel -t calhelp/calserver-api-v2:local .
# Frontend v2 image
docker build -f Dockerfile.frontend -t calhelp/calserver-frontend:local .
Environment Variables Reference¶
Required (in .env)¶
| Variable | Description | Example |
|---|---|---|
ENABLE_V2 |
Enable v2 services | true |
LARAVEL_APP_KEY |
Laravel encryption key | base64:abc123... |
APP_URL |
Base URL (must match DOMAIN) | https://develop.net-cal.com |
DB_PREFIX |
Table prefix of the V1 database | (empty if no prefix) |
Optional (Defaults Provided)¶
| Variable | Default | Description |
|---|---|---|
V2_STANDALONE |
false |
true = run V2 without V1: docker-compose.yml is not loaded at all, so neither the Yii container nor the V1 MySQL is started. Implies ENABLE_V2=true. See V2 autonomy check |
LETSENCRYPT_TEST |
(empty) | true = Let's Encrypt staging (no rate limit) |
APP_ENV |
production |
Laravel environment |
APP_DEBUG |
false |
Debug mode (never true in production!) |
PUSHER_APP_ID |
(from v1) | For live updates |
PUSHER_KEY |
(from v1) | For live updates |
PUSHER_SECRET |
(from v1) | For live updates |
PUSHER_CLUSTER |
(from v1) | For live updates |
Already Present (Shared with v1)¶
These variables are automatically passed through to v2:
- DB_HOST, DB_DATABASE, DB_USER, DB_PASSWORD (MySQL)
- DOMAIN, FILE_MANAGER_PATH
- PUSHER_* (real-time updates)
Disabling v2¶
# In .env:
ENABLE_V2=false
# Redeploy:
sudo ./deploy.sh
# Or manually stop v2 containers:
docker stop calserver-api-v2 calserver-frontend calserver-redis
docker rm calserver-api-v2 calserver-frontend calserver-redis
v1 then continues to run on its own — with no impact.
Troubleshooting¶
v2 API Not Responding¶
# Check container logs
docker logs calserver-api-v2 --tail 50
# Common cause: LARAVEL_APP_KEY is missing or invalid
docker exec calserver-api-v2 php artisan key:generate --show
# Is PHP-FPM running?
docker exec calserver-api-v2 ps aux | grep php-fpm
Frontend Shows Blank Page¶
# Check container logs
docker logs calserver-frontend --tail 50
# API base URL correct?
docker exec calserver-frontend printenv | grep NUXT
# NUXT_PUBLIC_API_BASE should be https://develop.net-cal.com/api/v2
Redis Connection Failed¶
# Is Redis running?
docker exec calserver-redis redis-cli ping
# → PONG
# Reachable from the API container?
docker exec calserver-api-v2 php -r "echo @fsockopen('redis', 6379) ? 'OK' : 'FAIL';"
Let's Encrypt Certificate Missing¶
# Check companion logs
docker logs letsencrypt-nginx-proxy --tail 30
# DNS correct?
dig +short develop.net-cal.com
# Port 80 open? (for HTTP-01 challenge)
curl -v http://develop.net-cal.com/.well-known/acme-challenge/test 2>&1 | head -20
Check Nginx Proxy Routing¶
# Show active Nginx config
docker exec nginx-proxy nginx -T 2>&1 | grep -B2 -A10 "location /api/v2"
# Check vhost.d files
docker exec nginx-proxy ls -la /etc/nginx/vhost.d/
# Reload Nginx (without restart)
docker exec nginx-proxy nginx -s reload
CI/CD Pipeline¶
The v2 services are automatically deployed by CI/CD:
- Push to
develop→ Images built and deployed with tagdevelop-latest - Push to
master/release/*→ Images built and deployed with tagrelease-latest
Pipeline Flow¶
┌───────────────────────────────────────────────────┐
│ GitHub Actions CI │
├───────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ build-v1 │ │ build-api-v2 │ │ build-fe │ │
│ │ (Yii) │ │ (Laravel) │ │ (Nuxt) │ │
│ └──────┬──────┘ └──────┬───────┘ └────┬─────┘ │
│ │ │ │ │
│ └────────┬───────┴───────────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ deploy-dev │ (SSH → deploy.sh) │
│ │ deploy-demo │ │
│ └────────────────┘ │
└───────────────────────────────────────────────────┘
The deploy.sh script detects ENABLE_V2=true on the target server and
automatically starts all v2 services as well.