233 lines
8 KiB
Markdown
233 lines
8 KiB
Markdown
# Deployment Guide — lilastudy.com
|
|
|
|
This document describes the production deployment of the lila vocabulary trainer on a Hetzner VPS.
|
|
|
|
## Infrastructure Overview
|
|
|
|
- **VPS**: Hetzner, Debian 13, ARM64 (aarch64), 4GB RAM
|
|
- **Domain**: lilastudy.com (DNS managed on Hetzner, wildcard `*.lilastudy.com` configured)
|
|
- **Reverse proxy**: Caddy (Docker container, automatic HTTPS via Let's Encrypt)
|
|
- **Container registry**: Forgejo built-in package registry
|
|
- **Git server**: Forgejo
|
|
|
|
### Subdomain Routing
|
|
|
|
| Subdomain | Service | Container port |
|
|
| ------------------- | ------------------------------------- | -------------- |
|
|
| `lilastudy.com` | Frontend (nginx serving static files) | 80 |
|
|
| `api.lilastudy.com` | Express API | 3000 |
|
|
| `git.lilastudy.com` | Forgejo (web UI + container registry) | 3000 |
|
|
|
|
### Ports Exposed to the Internet
|
|
|
|
| Port | Service |
|
|
| ---- | -------------------------------- |
|
|
| 80 | Caddy (HTTP, redirects to HTTPS) |
|
|
| 443 | Caddy (HTTPS) |
|
|
| 2222 | Forgejo SSH (git clone/push) |
|
|
|
|
All other services (Postgres, API, frontend) communicate only over the internal Docker network.
|
|
|
|
## VPS Base Setup
|
|
|
|
The server has SSH key auth, ufw firewall (ports 22, 80, 443, 2222), and fail2ban configured. Docker and Docker Compose are installed via Docker's official apt repository.
|
|
|
|
Locale `en_GB.UTF-8` was generated alongside `en_US.UTF-8` to suppress SSH locale warnings from the dev laptop.
|
|
|
|
## Directory Structure on VPS
|
|
|
|
```
|
|
~/lila-app/
|
|
├── docker-compose.yml
|
|
├── Caddyfile
|
|
└── .env
|
|
~/lila-db-backups/
|
|
├── lila-db-YYYY-MM-DD_HHMM.sql.gz
|
|
└── backup.sh
|
|
```
|
|
|
|
## Docker Compose Stack
|
|
|
|
All services run in a single `docker-compose.yml` on a shared `lila-network`. The app images are pulled from the Forgejo registry.
|
|
|
|
### Services
|
|
|
|
- **caddy** — reverse proxy, only container with published ports (80, 443)
|
|
- **api** — Express backend, image from `git.lilastudy.com/forgejo-lila/lila-api:latest`
|
|
- **web** — nginx serving Vite-built static files, image from `git.lilastudy.com/forgejo-lila/lila-web:latest`
|
|
- **database** — PostgreSQL with a named volume (`lila-db`) for persistence
|
|
- **forgejo** — git server + container registry, SSH on port 2222, data in named volume (`forgejo-data`)
|
|
|
|
### Key Design Decisions
|
|
|
|
- No ports exposed on internal services — only Caddy faces the internet
|
|
- Frontend is built to static files at Docker build time; no Node process in production
|
|
- `VITE_API_URL` is baked in during the Docker build via a build arg
|
|
- The API reads all environment-specific config from `.env` (CORS origin, auth URLs, DB connection, cookie domain)
|
|
|
|
## Environment Variables
|
|
|
|
Production `.env` on the VPS:
|
|
|
|
```
|
|
DATABASE_URL=postgres://postgres:PASSWORD@database:5432/lila
|
|
POSTGRES_USER=postgres
|
|
POSTGRES_PASSWORD=PASSWORD
|
|
POSTGRES_DB=lila
|
|
BETTER_AUTH_SECRET=GENERATED_SECRET
|
|
BETTER_AUTH_URL=https://api.lilastudy.com
|
|
CORS_ORIGIN=https://lilastudy.com
|
|
COOKIE_DOMAIN=.lilastudy.com
|
|
GOOGLE_CLIENT_ID=...
|
|
GOOGLE_CLIENT_SECRET=...
|
|
GITHUB_CLIENT_ID=...
|
|
GITHUB_CLIENT_SECRET=...
|
|
```
|
|
|
|
Note: `DATABASE_URL` host is `database` (the Docker service name). Password in `DATABASE_URL` must match `POSTGRES_PASSWORD`.
|
|
|
|
## Docker Images — Build and Deploy
|
|
|
|
Images are built on the dev laptop with cross-compilation for ARM64, pushed to the Forgejo registry, and pulled on the VPS.
|
|
|
|
### Build (on dev laptop)
|
|
|
|
```bash
|
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
|
|
docker build --platform linux/arm64 \
|
|
-t git.lilastudy.com/forgejo-lila/lila-api:latest \
|
|
--target runner -f apps/api/Dockerfile .
|
|
|
|
docker build --platform linux/arm64 \
|
|
-t git.lilastudy.com/forgejo-lila/lila-web:latest \
|
|
--target production \
|
|
--build-arg VITE_API_URL=https://api.lilastudy.com \
|
|
-f apps/web/Dockerfile .
|
|
```
|
|
|
|
QEMU registration may need to be re-run after Docker or system restarts.
|
|
|
|
### Push (from dev laptop)
|
|
|
|
```bash
|
|
docker login git.lilastudy.com
|
|
docker push git.lilastudy.com/forgejo-lila/lila-api:latest
|
|
docker push git.lilastudy.com/forgejo-lila/lila-web:latest
|
|
```
|
|
|
|
### Deploy (on VPS)
|
|
|
|
```bash
|
|
docker compose pull
|
|
docker compose up -d
|
|
```
|
|
|
|
To deploy a single service without restarting the whole stack:
|
|
|
|
```bash
|
|
docker compose pull api
|
|
docker compose up -d api
|
|
```
|
|
|
|
### Cleanup
|
|
|
|
Remove unused images after deployments:
|
|
|
|
```bash
|
|
docker image prune -f # safe — only removes dangling images
|
|
docker system prune -a # aggressive — removes all unused images
|
|
```
|
|
|
|
## Dockerfiles
|
|
|
|
### API (`apps/api/Dockerfile`)
|
|
|
|
Multi-stage build: base → deps → dev → builder → runner. The `runner` stage does a fresh `pnpm install --prod` to get correct symlinks. Output is at `apps/api/dist/src/server.js` due to monorepo rootDir configuration.
|
|
|
|
### Frontend (`apps/web/Dockerfile`)
|
|
|
|
Multi-stage build: base → deps → dev → builder → production. The `builder` stage compiles with `VITE_API_URL` baked in. The `production` stage is `nginx:alpine` serving static files from `dist/`. Includes a custom `nginx.conf` for SPA fallback routing (`try_files $uri $uri/ /index.html`).
|
|
|
|
## Monorepo Package Exports
|
|
|
|
Both `packages/shared` and `packages/db` have their `exports` in `package.json` pointing to compiled JavaScript (`./dist/src/...`), not TypeScript source. This is required for production builds where Node cannot run `.ts` files. In dev, packages must be built before running the API.
|
|
|
|
## Database
|
|
|
|
### Initial Seeding
|
|
|
|
The production database was initially populated via `pg_dump` from the dev laptop:
|
|
|
|
```bash
|
|
# On dev laptop
|
|
docker exec lila-database pg_dump -U USER DB > seed.sql
|
|
scp seed.sql lila@VPS_IP:~/lila-app/
|
|
|
|
# On VPS
|
|
docker exec -i lila-database psql -U postgres -d lila < seed.sql
|
|
```
|
|
|
|
### Ongoing Data Updates
|
|
|
|
The seeding script (`packages/db/src/seeding-datafiles.ts`) uses `onConflictDoNothing()` on all inserts, making it idempotent. New vocabulary data (e.g. Spanish words) can be added by running the seeding script against production — it inserts only new records without affecting existing data or user tables.
|
|
|
|
### Schema Migrations
|
|
|
|
Schema changes are managed by Drizzle. Deploy order matters:
|
|
|
|
1. Run migration first (database gets new structure)
|
|
2. Deploy new API image (code uses new structure)
|
|
|
|
Reversing this order causes the API to crash on missing columns/tables.
|
|
|
|
## Backups
|
|
|
|
A cron job runs daily at 3:00 AM, dumping the database to a compressed SQL file and keeping the last 7 days:
|
|
|
|
```bash
|
|
# ~/backup.sh
|
|
0 3 * * * /home/lila/backup.sh
|
|
```
|
|
|
|
Backups are stored in `~/backups/` as `lila-db-YYYY-MM-DD_HHMM.sql.gz`.
|
|
|
|
### Pulling Backups to Dev Laptop
|
|
|
|
A script on the dev laptop syncs new backups on login:
|
|
|
|
```bash
|
|
# ~/pull-backups.sh (runs via .profile on login)
|
|
rsync -avz --ignore-existing --include="*.sql.gz" --exclude="*" lila@VPS_IP:~/backups/ ~/lila-backups/
|
|
```
|
|
|
|
### Restoring from Backup
|
|
|
|
```bash
|
|
gunzip -c lila-db-YYYY-MM-DD_HHMM.sql.gz | docker exec -i lila-database psql -U postgres -d lila
|
|
```
|
|
|
|
## OAuth Configuration
|
|
|
|
Google and GitHub OAuth apps must have both dev and production redirect URIs:
|
|
|
|
- **Google Cloud Console**: Authorized redirect URIs include both `http://localhost:3000/api/auth/callback/google` and `https://api.lilastudy.com/api/auth/callback/google`
|
|
- **GitHub Developer Settings**: Authorization callback URL includes both localhost and production
|
|
|
|
## Forgejo SSH
|
|
|
|
The dev laptop's `~/.ssh/config` maps `git.lilastudy.com` to port 2222:
|
|
|
|
```
|
|
Host git.lilastudy.com
|
|
Port 2222
|
|
```
|
|
|
|
This allows standard git commands without specifying the port.
|
|
|
|
## Known Issues and Future Work
|
|
|
|
- **CI/CD**: Currently manual build-push-pull cycle. Plan: Forgejo Actions with a runner on the VPS building ARM images natively (eliminates QEMU cross-compilation)
|
|
- **Backups**: Offsite backup storage (Hetzner Object Storage or similar) should be added
|
|
- **Valkey**: Not in the production stack yet. Will be added when multiplayer requires session/room state
|
|
- **Monitoring/logging**: No centralized logging or uptime monitoring configured
|