# 06 — Deployment > **Purpose:** Condensed infrastructure reference for LLMs working on deployment, CI/CD, or ops tasks. For full setup details (VPS provisioning, Forgejo configuration, backup scripts), see the human-readable DEPLOYMENT.md. > **Last updated:** 2026-05-15 > **Depends on:** 00-project-overview.md --- ## Infrastructure Overview ``` Internet ↓ Caddy (Docker container, ports 80/443) ├── lilastudy.com → web container (nginx:alpine, static files) ├── api.lilastudy.com → api container (Express, port 3000) └── git.lilastudy.com → forgejo container (git + registry, port 3000) SSH (port 2222) → forgejo container (git push/pull) ``` **VPS:** Hetzner, Debian 13, ARM64 (aarch64), 4GB RAM **Domain:** lilastudy.com, wildcard `*.lilastudy.com` configured **Only Caddy faces the internet.** All other services communicate over internal Docker network. --- ## Docker Compose Stack Services on shared `lila-network`: | Service | Image | Ports (internal) | Notes | | -------- | ------------------------------------------------ | ---------------- | -------------------------------------------------- | | caddy | caddy:alpine | 80, 443 | Only container with published ports | | api | `git.lilastudy.com/forgejo-lila/lila-api:latest` | 3000 | Multi-stage Dockerfile, runs migrations on startup | | web | `git.lilastudy.com/forgejo-lila/lila-web:latest` | 80 | nginx:alpine, SPA fallback via try_files | | database | postgres:16 | 5432 | Named volume `lila-db` for persistence | | forgejo | forgejo:... | 3000, 2222 | Git + container registry, SSH on 2222 | **No ports exposed on internal services.** Only Caddy (80/443) and Forgejo SSH (2222) are public. --- ## Build & Deploy Flow ``` Dev laptop: git push to main ↓ Forgejo Actions triggers (runner on VPS) ↓ Build API image (target: runner) Build Web image (target: production, VITE_API_URL baked in) ↓ Push both to git.lilastudy.com registry ↓ SSH into VPS, docker compose pull, restart containers ↓ API container runs migrations on startup (migrate.js before server.js) ↓ App updated (~2–5 min total) ``` **Cross-compilation:** Images built natively on ARM64 VPS (no QEMU). Dev laptop used for initial pushes before CI/CD was set up. --- ## Environment-Driven Config Same code runs in dev and production. Environment variables control behavior: | Variable | Dev | Production | | ----------------- | ------------------------------------ | ------------------------------------------------- | | `DATABASE_URL` | `postgres://...@localhost:5432/lila` | `postgres://...@database:5432/lila` | | `BETTER_AUTH_URL` | `http://localhost:3000` | `https://api.lilastudy.com` | | `CORS_ORIGIN` | `http://localhost:5173` | `https://lilastudy.com` | | `COOKIE_DOMAIN` | undefined | `.lilastudy.com` | | `VITE_API_URL` | `http://localhost:3000` | `https://api.lilastudy.com` (baked at build time) | **Note:** `VITE_API_URL` is baked into the frontend at Docker build time via `--build-arg`. It cannot be changed at runtime. --- ## Database ### Migrations Drizzle migrations run automatically on API container startup. The Dockerfile entrypoint: ```dockerfile CMD ["node", "dist/src/migrate.js", "&&", "node", "dist/src/server.js"] ``` **Deploy order enforced automatically:** migrations before server starts. ### Backups - Daily cron job at 3:00 AM: `pg_dump` → compressed SQL → `~/backups/` - 7-day retention on VPS - Dev laptop auto-syncs new backups on login via `rsync` - **Offsite storage:** Planned (Hetzner Object Storage or S3-compatible) ### Seeding Idempotent (`onConflictDoNothing`). Safe to re-run for adding new languages without affecting existing data or user tables. --- ## Auth & OAuth **Better Auth** embedded in Express API. No separate auth service. **Social providers:** - Google OAuth — consent screen in testing mode (100 user cap). Must publish before reaching 80 users. - GitHub OAuth — configured for both dev and production redirect URIs **Cross-subdomain cookies:** `COOKIE_DOMAIN=.lilastudy.com` (leading dot) makes auth cookie valid across all subdomains. --- ## Known Issues & Limitations | Issue | Impact | Status | | --------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------- | | lila-web has no healthcheck | Vite dev server has no health endpoint; `depends_on` uses API healthcheck as proxy | Acceptable for dev | | Valkey memory overcommit warning | Harmless in dev. Fix before production: `vm.overcommit_memory = 1` | Documented | | No centralized monitoring/logging | No uptime alerts or log aggregation on VPS | Planned (BACKLOG.md) | | Backups only on VPS + dev laptop | No offsite protection against VPS failure | Planned (BACKLOG.md) | | Google OAuth in testing mode | 100 user cap | Must publish before 80 users | --- ## Key Files | File | Purpose | | ------------------------------- | ------------------------------------------------------ | | `docker-compose.yml` (root) | Local dev stack | | `docker-compose.yml` (VPS) | Production stack | | `apps/api/Dockerfile` | Multi-stage: deps → dev → builder → runner | | `apps/web/Dockerfile` | Multi-stage: deps → dev → builder → production (nginx) | | `apps/web/nginx.conf` | SPA fallback routing | | `Caddyfile` | Reverse proxy routing, automatic HTTPS | | `.forgejo/workflows/deploy.yml` | CI/CD pipeline | | `apps/api/src/migrate.ts` | Drizzle migration runner |