220 lines
8.6 KiB
YAML
220 lines
8.6 KiB
YAML
# Production stack on the Home Server (server.gib, rootful Docker), deployed
|
|
# as ~/Server/Gbrown/compose.yml. This file is the versioned source: edit it
|
|
# here and copy it over, never the other way round.
|
|
#
|
|
# ~/Server/Gbrown holds only this file, .env and data/. Every bind mount lives
|
|
# under data/ (data/convex, data/gibtv/channel, data/gibtv/nginx.conf, copied
|
|
# from apps/gibtv/nginx.conf). No source or build context lives on the server:
|
|
# app images come from the Gitea registry. CI redeploys at once through the
|
|
# forced-command key behind ~/Server/ci-deploy.sh (`docker compose pull && up -d`).
|
|
#
|
|
# Secrets come from ~/Server/Gbrown/.env (mode 600), written from Infisical
|
|
# `prod` by scripts/write-prod-env. Never commit or print it.
|
|
#
|
|
# NPM reaches everything over nginx-bridge, so nothing publishes a host port:
|
|
# api.gbrown.org -> http://gbrown-backend:3210
|
|
# convex.gbrown.org -> http://gbrown-backend:3211
|
|
# dashboard.gbrown.org -> http://gbrown-dashboard:6791
|
|
# gbrown.org (host 119) -> http://gbrown-next:3000 (at cutover)
|
|
# tv.gbrown.org -> http://gbrown-gibtv-web:80 (phase 8)
|
|
#
|
|
# Watchtower (label-enabled, 04:00) updates the app images. The Convex backend,
|
|
# its dashboard and the socket proxy are pinned and opt out: a backend upgrade
|
|
# migrates its data, so it happens on purpose, not overnight.
|
|
#
|
|
# The `tools` profile keeps the one-off normalize pass out of a plain `up -d`
|
|
# and `pull`. Run it with `docker compose run --rm gbrown-gibtv-normalize`.
|
|
name: gbrown
|
|
|
|
networks:
|
|
nginx-bridge:
|
|
external: true
|
|
# The agent and its socket proxy only. No route out.
|
|
agent-socket:
|
|
internal: true
|
|
|
|
volumes:
|
|
# The live HLS window is about 10 MB and rewritten every 6 s. Keep it in RAM.
|
|
gibtv-hls:
|
|
driver_opts:
|
|
type: tmpfs
|
|
device: tmpfs
|
|
o: size=64m,uid=1000,gid=1000
|
|
|
|
services:
|
|
gbrown-backend:
|
|
# Pinned to the build the VPS Convex stacks run (bumped 2026-10-28). Bump on purpose.
|
|
image: ghcr.io/convex/convex-backend@sha256:207cf95def42f01614b6e3af0af5c20d0dcaa5ff2adf258ee7bc9555576644fa
|
|
container_name: gbrown-backend
|
|
hostname: gbrown-backend
|
|
networks: ['nginx-bridge']
|
|
volumes: ['./data/convex:/convex/data']
|
|
# No TZ: the backend panics unless it runs in UTC.
|
|
environment:
|
|
- INSTANCE_NAME=${INSTANCE_NAME:?}
|
|
- INSTANCE_SECRET=${INSTANCE_SECRET:?}
|
|
- CONVEX_CLOUD_ORIGIN=https://api.gbrown.org
|
|
- CONVEX_SITE_ORIGIN=https://convex.gbrown.org
|
|
- DISABLE_BEACON=true
|
|
- REDACT_LOGS_TO_CLIENT=true
|
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ['CMD', 'curl', '-f', 'http://localhost:3210/version']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
stop_grace_period: 10s
|
|
stop_signal: SIGINT
|
|
|
|
gbrown-dashboard:
|
|
# Released alongside the backend; pinned with it.
|
|
image: ghcr.io/convex/convex-dashboard@sha256:a55bf8abca1332f787bc4d4a79850182175027be7b70aa230be4389a1d3268e2
|
|
container_name: gbrown-dashboard
|
|
hostname: gbrown-dashboard
|
|
networks: ['nginx-bridge']
|
|
environment:
|
|
# The browser talks to the backend directly, so this is the public URL.
|
|
- NEXT_PUBLIC_DEPLOYMENT_URL=https://api.gbrown.org
|
|
depends_on:
|
|
gbrown-backend:
|
|
condition: service_healthy
|
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
|
restart: unless-stopped
|
|
stop_grace_period: 10s
|
|
stop_signal: SIGINT
|
|
|
|
gbrown-next:
|
|
image: git.gbrown.org/gib/gbrown-next:latest
|
|
container_name: gbrown-next
|
|
hostname: gbrown-next
|
|
networks: ['nginx-bridge']
|
|
environment:
|
|
- TZ=America/New_York
|
|
- NODE_ENV=production
|
|
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
|
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
|
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL}
|
|
- NEXT_PUBLIC_PLAUSIBLE_SRC=${NEXT_PUBLIC_PLAUSIBLE_SRC:-}
|
|
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY:-}
|
|
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST:-}
|
|
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
|
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
|
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG}
|
|
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 20s
|
|
|
|
# The agent never sees the Docker socket. The proxy answers only
|
|
# GET /containers/json, /events, /_ping and /version (with or without an API
|
|
# version prefix), refuses everything else, and only accepts connections from
|
|
# the gbrown-agent hostname on the internal agent-socket network.
|
|
# Docker here applies no SELinux labels to containers, so mounts need no :z.
|
|
gbrown-agent-socket-proxy:
|
|
image: docker.io/wollomatic/socket-proxy:1.13.1
|
|
container_name: gbrown-agent-socket-proxy
|
|
hostname: gbrown-agent-socket-proxy
|
|
networks: ['agent-socket']
|
|
command:
|
|
- '-loglevel=info'
|
|
- '-listenip=0.0.0.0'
|
|
- '-allowfrom=gbrown-agent'
|
|
- '-allowGET=/(v1\.[0-9]+/)?(containers/json|events|_ping|version)'
|
|
- '-allowhealthcheck'
|
|
# Exit if the socket breaks (a Docker upgrade) so Docker restarts it.
|
|
- '-watchdoginterval=3600'
|
|
- '-stoponwatchdog'
|
|
- '-shutdowngracetime=5'
|
|
# nobody, plus gid 963: the host's docker group, which owns the socket.
|
|
user: '65534:963'
|
|
volumes: ['/var/run/docker.sock:/var/run/docker.sock:ro']
|
|
read_only: true
|
|
mem_limit: 64M
|
|
cap_drop: [ALL]
|
|
security_opt: ['no-new-privileges:true']
|
|
healthcheck:
|
|
test: ['CMD', './healthcheck']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 2
|
|
# Pinned on purpose. Watchtower must not swap the gatekeeper on its own.
|
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
|
restart: unless-stopped
|
|
|
|
gbrown-agent:
|
|
image: git.gbrown.org/gib/gbrown-agent:latest
|
|
container_name: gbrown-agent
|
|
hostname: gbrown-agent
|
|
# nginx-bridge to reach gbrown-backend, agent-socket to reach the proxy.
|
|
networks: ['nginx-bridge', 'agent-socket']
|
|
environment:
|
|
- TZ=America/New_York
|
|
- HOST_ID=home
|
|
- DOCKER_HOST=tcp://gbrown-agent-socket-proxy:2375
|
|
# Straight to the backend's HTTP actions port, no proxy in between.
|
|
- INGEST_URL=http://gbrown-backend:3211/ingest
|
|
- AGENT_INGEST_SECRET=${AGENT_INGEST_SECRET:?}
|
|
# The UniFi gateway on the LAN. UNIFI_CA is its own certificate, the only
|
|
# one the agent trusts there. Leave all three unset to skip the network.
|
|
- UNIFI_URL=${UNIFI_URL:-}
|
|
- UNIFI_API_KEY=${UNIFI_API_KEY:-}
|
|
- UNIFI_CA=${UNIFI_CA:-}
|
|
volumes: ['/home/gib/Server/Nginx/volumes/data/logs:/npm-logs:ro']
|
|
depends_on:
|
|
gbrown-agent-socket-proxy:
|
|
condition: service_healthy
|
|
read_only: true
|
|
cap_drop: [ALL]
|
|
security_opt: ['no-new-privileges:true']
|
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
|
restart: unless-stopped
|
|
stop_grace_period: 10s
|
|
|
|
# gib.tv (apps/gibtv). Nothing here touches the media library except the
|
|
# normalize pass, which mounts it read-only. The streamer stream-copies, so
|
|
# CPU is near zero; each viewer pulls about 1 Mbps of home upload.
|
|
gbrown-gibtv:
|
|
image: git.gbrown.org/gib/gbrown-gibtv:latest
|
|
container_name: gbrown-gibtv
|
|
hostname: gbrown-gibtv
|
|
# No network needed: it only writes the HLS window to the shared volume.
|
|
network_mode: none
|
|
environment: ['TZ=America/New_York']
|
|
volumes:
|
|
- ./data/gibtv/channel:/media/channel:ro
|
|
- gibtv-hls:/srv/hls
|
|
cpus: 0.5
|
|
mem_limit: 256m
|
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
|
restart: unless-stopped
|
|
|
|
gbrown-gibtv-web:
|
|
image: docker.io/library/nginx:alpine
|
|
container_name: gbrown-gibtv-web
|
|
hostname: gbrown-gibtv-web
|
|
networks: ['nginx-bridge']
|
|
depends_on: [gbrown-gibtv]
|
|
volumes:
|
|
- gibtv-hls:/usr/share/nginx/html:ro
|
|
- ./data/gibtv/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
|
restart: unless-stopped
|
|
|
|
# One-off: `docker compose run --rm gbrown-gibtv-normalize`. Writes one
|
|
# 640x480 h264 file per episode, skips finished files, safe to resume.
|
|
gbrown-gibtv-normalize:
|
|
image: git.gbrown.org/gib/gbrown-gibtv:latest
|
|
profiles: ['tools']
|
|
command: ['nice', '-n', '19', 'bun', 'src/cli/normalize.ts']
|
|
network_mode: none
|
|
volumes:
|
|
- '/home/gib/Public/TV/Dragnet (1951):/media/source:ro'
|
|
- ./data/gibtv/channel:/media/channel
|