|
| 1 | +# Server Contract |
| 2 | + |
| 3 | +This document defines the contract between the platform infrastructure (bootstrapped by `bootstrap-server.sh`) and the app deployment workflows (`deploy.yml`, `preview.yml`). It explains the directory layout, lifecycle, and assumptions that connect them. |
| 4 | + |
| 5 | +## Server Directory Layout |
| 6 | + |
| 7 | +``` |
| 8 | +/opt/platform/ # Platform root (created by bootstrap-server.sh) |
| 9 | + docker-compose.yml # Platform services (postgres, redis, minio, caddy, loki, promtail, grafana) |
| 10 | + .env # Platform credentials (POSTGRES_PASSWORD, MINIO_ROOT_*, GRAFANA_ADMIN_PASSWORD, ACME_EMAIL) |
| 11 | + .bootstrapped # Timestamp marker from last bootstrap run |
| 12 | + Caddyfile # Global Caddyfile — imports /etc/caddy/apps/*.caddy |
| 13 | + caddy-apps/ # Per-app Caddyfile fragments (written by deploy/preview workflows) |
| 14 | + <app-name>.caddy # Production route for an app |
| 15 | + <app-name>-pr-<N>.caddy # Preview route for a PR |
| 16 | + ops.caddy # Grafana route (created by bootstrap) |
| 17 | + credentials/ # Per-app credential files (created by create-app-credentials.sh) |
| 18 | + <app-name>.env # DB_USER, DB_PASSWORD, S3_ACCESS_KEY, S3_SECRET_KEY |
| 19 | + infrastructure/ # Ops scripts (copied from platform repo by bootstrap) |
| 20 | + backup-postgres.sh |
| 21 | + restore-postgres.sh |
| 22 | + check-alerts.sh |
| 23 | + update-images.sh |
| 24 | + create-app-credentials.sh |
| 25 | + usage-report.sh |
| 26 | + loki-config.yml # Loki configuration |
| 27 | + promtail-config.yml # Promtail configuration |
| 28 | + grafana/ # Grafana provisioning files and dashboards |
| 29 | + provisioning/datasources/ |
| 30 | + provisioning/dashboards/ |
| 31 | + dashboards/ |
| 32 | +
|
| 33 | +/opt/apps/ # Application root (created by bootstrap-server.sh) |
| 34 | + <app-name>/ # Cloned app repo (created manually by admin) |
| 35 | + deploy/ |
| 36 | + .env # App runtime config (created manually from env.template) |
| 37 | + docker-compose.yml # App services (joins towlion network) |
| 38 | + <app-name>-pr-<N>/ # Preview clone (created/destroyed by preview.yml) |
| 39 | +
|
| 40 | +/data/ # Persistent data volumes (created by bootstrap-server.sh) |
| 41 | + postgres/ # PostgreSQL data directory |
| 42 | + redis/ # Redis data directory |
| 43 | + minio/ # MinIO object storage |
| 44 | + caddy/data/ # Caddy TLS certificates |
| 45 | + caddy/config/ # Caddy config state |
| 46 | + loki/ # Loki log storage |
| 47 | + grafana/ # Grafana state |
| 48 | + backups/postgres/ # pg_dump backup files (7-day retention) |
| 49 | +``` |
| 50 | + |
| 51 | +## Bootstrap to Deploy Lifecycle |
| 52 | + |
| 53 | +1. **Bootstrap the server** — Run `sudo bash infrastructure/bootstrap-server.sh` on a fresh Debian 12 machine. This creates the directory layout above, installs Docker, creates the `deploy` user, generates platform credentials, starts the 7 platform services, copies infrastructure scripts, and installs cron jobs. |
| 54 | + |
| 55 | +2. **Configure DNS** — Point app domains and `*.preview.<domain>` to the server IP. |
| 56 | + |
| 57 | +3. **Clone the app repo** — SSH in as `deploy` and clone the app to `/opt/apps/<name>/`. |
| 58 | + |
| 59 | +4. **Create `deploy/.env`** — Copy `deploy/env.template` and fill in values (DATABASE_URL, S3 credentials, etc.). |
| 60 | + |
| 61 | +5. **Provision per-app credentials (optional)** — Run `create-app-credentials.sh <name>` to create an isolated PostgreSQL user and MinIO bucket. Credentials are written to `/opt/platform/credentials/<name>.env`. |
| 62 | + |
| 63 | +6. **Configure GitHub secrets** — Set `SERVER_HOST`, `SERVER_USER`, `SERVER_SSH_KEY`, and `APP_DOMAIN` on the app repo. Add `PREVIEW_DOMAIN` for preview environments. |
| 64 | + |
| 65 | +7. **Push to main** — Triggers `deploy.yml`: |
| 66 | + - SSHes into the server as `deploy` |
| 67 | + - `cd /opt/apps/<name> && git pull origin main` |
| 68 | + - Creates the app database if it doesn't exist (via platform postgres) |
| 69 | + - Sources per-app credentials from `/opt/platform/credentials/<name>.env` (if present) and updates `deploy/.env` with isolated DB/S3 values |
| 70 | + - `docker compose -p <name> -f deploy/docker-compose.yml up -d --build` |
| 71 | + - Runs Alembic migrations inside the app container |
| 72 | + - Writes a Caddyfile to `/opt/platform/caddy-apps/<name>.caddy` |
| 73 | + - Reloads Caddy to pick up the new route |
| 74 | + |
| 75 | +## App Workflow Server Assumptions |
| 76 | + |
| 77 | +The `deploy.yml` and `preview.yml` workflows SSH into the server and depend on the following structure being in place: |
| 78 | + |
| 79 | +| Path / Resource | Purpose | Created By | |
| 80 | +|---|---|---| |
| 81 | +| `/opt/platform/docker-compose.yml` | Platform services (postgres, redis, caddy, etc.) | `bootstrap-server.sh` | |
| 82 | +| `/opt/platform/.env` | `POSTGRES_PASSWORD` for database operations | `bootstrap-server.sh` | |
| 83 | +| `/opt/platform/caddy-apps/` | Writable directory for per-app Caddyfile fragments | `bootstrap-server.sh` | |
| 84 | +| `/opt/platform/credentials/<name>.env` | Per-app DB/S3 credentials (optional) | `create-app-credentials.sh` | |
| 85 | +| `/opt/apps/<name>/` | Cloned app repo with `deploy/.env` configured | Admin (manual) | |
| 86 | +| `towlion` Docker network | Shared network connecting platform services and app containers | `bootstrap-server.sh` | |
| 87 | +| `deploy` user | SSH user with Docker group membership | `bootstrap-server.sh` | |
| 88 | + |
| 89 | +If any of these are missing, the workflow will fail. The bootstrap script is idempotent and can be re-run to restore missing structure. |
| 90 | + |
| 91 | +## Infrastructure Scripts Reference |
| 92 | + |
| 93 | +All scripts live in the platform repo under `infrastructure/` and are copied to `/opt/platform/infrastructure/` during bootstrap. |
| 94 | + |
| 95 | +| Script | Purpose | Invocation | |
| 96 | +|---|---|---| |
| 97 | +| `bootstrap-server.sh` | Transform fresh Debian 12 into running platform | Manual (`sudo bash`) | |
| 98 | +| `verify-server.sh` | Read-only health check of server state | Manual (`bash`) | |
| 99 | +| `create-app-credentials.sh` | Provision per-app PostgreSQL user + MinIO bucket | Manual (`bash <script> <app-name>`) | |
| 100 | +| `backup-postgres.sh` | Per-database `pg_dump` with 7-day retention | Cron: daily at 02:00 | |
| 101 | +| `restore-postgres.sh` | Restore a database from backup | Manual (`bash <script>`) | |
| 102 | +| `check-alerts.sh` | Check container health, disk, memory; create GitHub Issues | Cron: every 5 minutes | |
| 103 | +| `update-images.sh` | Pull latest Docker images and recreate containers | Cron: weekly Sunday at 03:00 | |
| 104 | +| `usage-report.sh` | Generate 6-section resource usage report | Manual (`bash`) | |
| 105 | + |
| 106 | +## Caddyfile Generation |
| 107 | + |
| 108 | +The platform Caddyfile at `/opt/platform/Caddyfile` contains a single import directive: |
| 109 | + |
| 110 | +``` |
| 111 | +{ |
| 112 | + email {$ACME_EMAIL:admin@localhost} |
| 113 | +} |
| 114 | +
|
| 115 | +import /etc/caddy/apps/*.caddy |
| 116 | +``` |
| 117 | + |
| 118 | +The `caddy-apps/` directory is bind-mounted into the Caddy container at `/etc/caddy/apps/`. App workflows write per-app `.caddy` files into this directory. |
| 119 | + |
| 120 | +**Production** (`deploy.yml`) writes `/opt/platform/caddy-apps/<name>.caddy`: |
| 121 | + |
| 122 | +``` |
| 123 | +app.example.com { |
| 124 | + reverse_proxy <name>-app-1:8000 |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +**Preview** (`preview.yml`) writes `/opt/platform/caddy-apps/<name>-pr-<N>.caddy`: |
| 129 | + |
| 130 | +``` |
| 131 | +pr-<N>.preview.example.com { |
| 132 | + reverse_proxy <name>-pr-<N>-app-1:8000 |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +After writing the file, both workflows reload Caddy: |
| 137 | + |
| 138 | +```bash |
| 139 | +docker compose -f /opt/platform/docker-compose.yml exec -T caddy caddy reload --config /etc/caddy/Caddyfile |
| 140 | +``` |
| 141 | + |
| 142 | +Preview cleanup removes the `.caddy` file and reloads Caddy again. |
| 143 | + |
| 144 | +## Per-App Credentials |
| 145 | + |
| 146 | +By default, apps connect to PostgreSQL as the `postgres` superuser (credentials from `deploy/.env`). For credential isolation, run: |
| 147 | + |
| 148 | +```bash |
| 149 | +bash /opt/platform/infrastructure/create-app-credentials.sh <app-name> |
| 150 | +``` |
| 151 | + |
| 152 | +This creates: |
| 153 | + |
| 154 | +- **PostgreSQL**: A dedicated user (`<app_name>_user`) with access restricted to `<app_name>_db` |
| 155 | +- **MinIO**: A dedicated user (`<app-name>-user`) with a scoped policy limiting access to the `<app-name>-uploads` bucket |
| 156 | +- **Credentials file**: `/opt/platform/credentials/<app-name>.env` containing `DB_USER`, `DB_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` (mode 600, owned by `deploy`) |
| 157 | + |
| 158 | +On subsequent deploys, `deploy.yml` checks for this credentials file and, if found, updates `deploy/.env` with the per-app values via `sed`: |
| 159 | + |
| 160 | +```bash |
| 161 | +CREDENTIALS_FILE="/opt/platform/credentials/${APP_NAME}.env" |
| 162 | +if [ -f "$CREDENTIALS_FILE" ]; then |
| 163 | + source "$CREDENTIALS_FILE" |
| 164 | + sed -i "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${APP_DB}|" deploy/.env |
| 165 | + sed -i "s|^S3_ACCESS_KEY=.*|S3_ACCESS_KEY=${S3_ACCESS_KEY}|" deploy/.env |
| 166 | + sed -i "s|^S3_SECRET_KEY=.*|S3_SECRET_KEY=${S3_SECRET_KEY}|" deploy/.env |
| 167 | + sed -i "s|^S3_BUCKET=.*|S3_BUCKET=${APP_NAME}-uploads|" deploy/.env |
| 168 | +fi |
| 169 | +``` |
| 170 | + |
| 171 | +If no credentials file exists, the workflow falls back to whatever is already in `deploy/.env` and logs a warning. |
0 commit comments