# Deploy — occupymedia.cc **Storage phase-one addendum:** the current deployment uses separate app/chat/ PeerTube CTs. New files/docs work is an independent, undeployed scaffold; see [files runbook](files/README.md) and [storage contracts](../../docs/occupymedia-storage.md). The older one-box instructions below do not deploy that stack. Live conduwuit v0.5.0 still has local media; its volumes require backup. R2 durability alone does not back up media or the metadata needed to resolve it. One box, everything behind the Cloudflare tunnel (already provisioned: zone `occupymedia.cc`, tunnel `occupymedia` with ingress app/api/tube/chat, R2 buckets `occupymedia-media-encrypted`, `-peertube-videos`, `-peertube-playlists`, `-matrix-media`). No inbound ports. All local ports bind to 127.0.0.1. ## WP3 release on the app CT (150) Use `docker compose -f docker-compose.app.yml` in this directory. Before syncing source, create and verify an owner `pg_dump` in `/opt/tactical-media-broadcast-hub/backups/`. Preserve the previous source for rollback. Both `DATABASE_URL` and `SCHEDULER_DATABASE_URL` in the CT `.env` must connect as `sp_app`, without superuser or BYPASSRLS privileges. Provision its password with owner access and keep real values only in the CT `.env`. Keep the existing `SUPERPOSTER_KEK`; scheduler inherits it and `TEMPORAL_ADDRESS`. Stop the old scheduler, then run the new API migration CLI offline with `SUPER_DATABASE_URL` set to the owner URL: `pnpm --filter @tactical-media-broadcast-hub/api exec tsx src/db/migrate.cli.ts --dir migrations`. The CLI skips recorded migrations; 019 tracks resumable transactional, batch, and concurrent-index phases. Confirm 018 and 019 in `schema_migrations` and check `assertPublishingSchema` using the runtime URL before starting new code. Build `api scheduler web`, then run `up -d --no-deps api`, followed by `scheduler` and `web`. Verify API health/readiness, invite regressions, an existing username/password login, scheduler readiness and Temporal connectivity, and the public web app. Defer production publish tests until explicitly requested. For rollback, restore source from the previous commit, re-sync, rebuild and restart the three services; do not undo migrations automatically. ## 0. Prerequisites on the box - Debian 12 / Ubuntu 24.04, 4+ vCPU, 8 GB RAM, 100 GB disk - Docker Engine + compose plugin (`curl -fsSL https://get.docker.com | sh`) - git ## 1. Get the code + secrets ```bash git clone git@gitlab.example.com:mediatools/tacticalmediahub.git cd tactical-media-broadcast-hub/infra/occupymedia cp .env.example .env # Fill: CLOUDFLARE_TUNNEL_TOKEN, R2_ACCESS_KEY, R2_SECRET_KEY (dashboard → R2 → # Manage API Tokens → Object Read & Write, scoped to the occupymedia-* buckets), # POSTGRES_PASSWORD, PEERTUBE_DB_PASSWORD, S3_KEK (openssl rand -hex 32 — ONE TIME, # it wraps every stored blob; lose it = archive unreadable) # peertube.env: set both PEERTUBE_DB_PASSWORD / POSTGRES_PASSWORD lines to the # value from .env, and set a registration/SMTP sender if you have them. ``` ## 2. Build + start the core ```bash docker compose build api scheduler web media-worker docker compose up -d postgres redis temporal peertube-postgres ``` ## 3. Apply API migrations (ordered, once) ```bash cd ../.. for f in apps/api/migrations/*.sql; do docker compose -f infra/occupymedia/docker-compose.yml exec -T postgres \ psql -U "$(grep -oP '(?<=POSTGRES_USER:)\S+' infra/occupymedia/docker-compose.yml | head -1 | tr -d '${:-occupymedia}')" \ -d occupymedia -v ON_ERROR_STOP=1 < "$f" || { echo "FAILED at $f"; break; } done ``` (If psql errors on an already-applied migration — re-runs are idempotent-safe for most files; check the failing file before continuing.) ## 4. Bring everything up ```bash docker compose -f infra/occupymedia/docker-compose.yml up -d docker compose ps # everything Up/healthy ``` ## 5. First-boot steps 1. **Tunnel**: `docker compose logs cloudflared` — should show 4 connections registered. Then `https://app.occupymedia.cc` (control room), `/healthz` on api., tube., chat. should all answer. 2. **PeerTube admin**: `docker compose logs peertube 2>&1 | grep -i -A3 password` — random root password printed ONCE. Log in at tube.occupymedia.cc, change it, then run the brand sync (`docs/peertube-setup.md`) and create the live channel. 3. **Matrix admin**: register the first account at chat.occupymedia.cc with the token from conduwuit.toml, then set `allow_registration = false` and `docker compose restart conduwuit`. Configure Element as client (`docs/matrix-setup.md`). 4. **Telegram bridge** (optional now): `docker compose --profile bridges run --rm mautrix-telegram` → interactive login with the bot/user token; then `docker compose --profile bridges up -d mautrix-telegram`. ## 6. Live ingest (deferred) RTMP/SRT cannot traverse the tunnel. Options documented in the session notes: tiny relay VPS + WireGuard to this box (recommended; keeps the home IP out of DNS), or direct port-forward. Until then the 24/7 channel runs playlists/VOD (media-worker pushes to PeerTube internally over the docker network). ## 7. Operations - **Backup**: `docker compose exec postgres pg_dump -U occupymedia occupymedia | gzip > backup-$(date +%F).sql.gz` + `tar` the named volumes (pgdata, peertube_pgdata, conduwuit_data). Media itself is in R2 (durable) — KEK backup matters more than volume backup. - **Update**: `git pull && docker compose build api scheduler web media-worker && docker compose up -d` (migrations: repeat step 3 for NEW files only). - **Logs**: `docker compose logs -f --tail=100 `. ## Known simplifications (first-production-pass) - observability (otel/prometheus/grafana) not included — add from infra/docker-compose.yml services when needed - single app instance per service (no horizontal scaling — intended at this size) - file-sharing/collab layer: oCIS S3NG + optional Collabora scaffold in `files/compose.yml`; deployment and Matrix migration remain separate prerequisites