chore(deploy): hardened prod compose, rollout script, and CI for asgard
docker-compose.prod.yml targets the public VPS: Postgres is never published, the app binds to 127.0.0.1 (Caddy proxies it), and every service has a memory/CPU cap plus no-new-privileges / cap_drop / read-only rootfs. deploy/deploy.sh rolls out main with a host-side build (migrations run via the entrypoint). CI type-checks and builds the frontend and byte-compiles the backend on every push to main.
This commit is contained in:
parent
2a6fde86a1
commit
3815c094a1
4 changed files with 180 additions and 0 deletions
49
deploy/README.md
Normal file
49
deploy/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Deploying Subfeed to the public VPS (the VPS)
|
||||
|
||||
The public test instance runs on the the VPS VPS at **https://subfeed.b1fr0st.eu**, behind
|
||||
Caddy (which terminates TLS and reverse-proxies to the app on `127.0.0.1:8080`).
|
||||
|
||||
## Layout on the VPS
|
||||
|
||||
```
|
||||
/srv/subfeed/
|
||||
src/ git clone of this repo (build context + compose file)
|
||||
.env secrets, mode 0600 — NEVER committed
|
||||
```
|
||||
|
||||
The hardened compose is [`docker-compose.prod.yml`](../docker-compose.prod.yml): Postgres is
|
||||
never published, the app binds to localhost only, and every service is memory/CPU-capped for
|
||||
the small VPS.
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. DNS `A`/`AAAA` for `subfeed` → the VPS (done via the your DNS provider CLI).
|
||||
2. Caddy vhost block `subfeed.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }` (+ the shared
|
||||
security-headers snippet), then reload Caddy.
|
||||
3. `git clone` this repo to `/srv/subfeed/src`.
|
||||
4. Create `/srv/subfeed/.env` (mode 0600) with the required keys — see
|
||||
[`.env.example`](../.env.example). Generate the secrets:
|
||||
- `SECRET_KEY`: `python -c "import secrets; print(secrets.token_urlsafe(48))"`
|
||||
- `TOKEN_ENCRYPTION_KEY`: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`
|
||||
- `POSTGRES_PASSWORD`: any long random string.
|
||||
- `OAUTH_REDIRECT_URL=https://subfeed.b1fr0st.eu/auth/callback`
|
||||
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `YOUTUBE_API_KEY` from Google Cloud Console.
|
||||
- `ALLOWED_EMAILS` / `ADMIN_EMAILS` = the invited Google accounts.
|
||||
|
||||
> The app refuses to start in production (an `https` redirect URL) if `SECRET_KEY` is the
|
||||
> placeholder/too short or `TOKEN_ENCRYPTION_KEY` is missing — by design.
|
||||
|
||||
## Rolling out a new version
|
||||
|
||||
```bash
|
||||
/srv/subfeed/src/deploy/deploy.sh
|
||||
```
|
||||
|
||||
It pulls `main`, rebuilds the image on the host, and restarts the stack (migrations run
|
||||
automatically via the entrypoint's `alembic upgrade head`).
|
||||
|
||||
## CI
|
||||
|
||||
`.forgejo/workflows/ci.yml` type-checks/builds the frontend and byte-compiles the backend on
|
||||
every push to `main`. It does not auto-deploy; rollout is the script above. (A future
|
||||
improvement: a Forgejo-triggered or watchtower-based auto-rollout.)
|
||||
20
deploy/deploy.sh
Executable file
20
deploy/deploy.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
# Roll out the latest main branch to the public the VPS instance.
|
||||
#
|
||||
# Layout on the VPS:
|
||||
# /srv/subfeed/src git clone of this repo (build context + compose file)
|
||||
# /srv/subfeed/.env secrets, mode 0600 (never in git)
|
||||
#
|
||||
# Build runs on the host Docker daemon, so it isn't constrained by the CI runner's memory.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=/srv/subfeed
|
||||
cd "$ROOT/src"
|
||||
|
||||
git fetch --quiet origin
|
||||
git checkout --quiet main
|
||||
git pull --ff-only --quiet
|
||||
|
||||
docker compose -f docker-compose.prod.yml --env-file "$ROOT/.env" up -d --build
|
||||
docker image prune -f >/dev/null || true
|
||||
docker compose -f docker-compose.prod.yml --env-file "$ROOT/.env" ps
|
||||
Loading…
Add table
Add a link
Reference in a new issue