Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
6 KiB
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 and storage contracts. 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
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
docker compose build api scheduler web media-worker
docker compose up -d postgres redis temporal peertube-postgres
3. Apply API migrations (ordered, once)
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
docker compose -f infra/occupymedia/docker-compose.yml up -d
docker compose ps # everything Up/healthy
5. First-boot steps
- Tunnel:
docker compose logs cloudflared— should show 4 connections registered. Thenhttps://app.occupymedia.cc(control room),/healthzon api., tube., chat. should all answer. - 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. - Matrix admin: register the first account at chat.occupymedia.cc with the
token from conduwuit.toml, then set
allow_registration = falseanddocker compose restart conduwuit. Configure Element as client (docs/matrix-setup.md). - Telegram bridge (optional now):
docker compose --profile bridges run --rm mautrix-telegram→ interactive login with the bot/user token; thendocker 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+tarthe 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 <service>.
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