Reelsmith¶
The reelsmith gateway publishes short videos to Instagram, Facebook, YouTube and TikTok on a
schedule, answers comments with DMs, and serves the files Meta fetches while it creates a post.
A render pipeline on verksted produces the videos each night and hands them to the gateway over
/api. Only the gateway runs here; its source, CI and the render pipeline live in the separate
mortennordbye/reelsmith repo. The manifests are in
k8s/talos/apps/reelsmith/.
| Part | Where |
|---|---|
| Namespace | reelsmith, Pod Security restricted |
| Deployment | reelsmith-gateway, one replica, Recreate, port 8000 |
| Image | ghcr.io/mortennordbye/reelsmith-gateway, tag in the images: block of kustomization.yaml |
| Public host | https://gate.nordbye.it, public Traefik gateway, Cloudflare edges only |
| Control panel | https://reelsmith.local.bigd.no/admin/, private gateway, behind Authentik |
| State | PVC reelsmith-state (syno-nfs-csi, 1Gi): SQLite at /state/gateway.sqlite3, covers in /state/covers |
| Backup copies | /state/backups, and PV reelsmith-offsite-backups mounted at /offsite |
| Secrets | ExternalSecret reelsmith-secret |
| Alerts | monitoring.yaml, sent to Discord at every severity |
Deployment¶
Kargo project reelsmith-cd promotes new gateway images straight to prod through a pull
request against this repo, then curls https://gate.nordbye.it/healthz as the smoke test
(kargo-projects/reelsmith.yaml,
kargo.md). Nothing deploys until that PR is merged. The inline
tag in deployment.yaml is only a fallback;
the images: block wins.
The pod runs as uid 10001 with a read-only root filesystem and a writable /tmp emptyDir.
Reloader restarts it when reelsmith-config or reelsmith-secret changes.
It stays at one replica. SQLite has a single writer, and the comment poller and the scheduler are
singletons, so two pods would both reply and both publish. Recreate keeps a rollout from running
two pods for a moment. Scaling out needs Postgres and leader election first.
Exposure¶
httproute.yaml serves gate.nordbye.it on
the public gateway as an allowlist; any other path is a 404 at Traefik.
| Path | Match | Used by |
|---|---|---|
/webhook |
prefix | Meta subscription handshake and deliveries |
/media, /covers |
prefix | Meta fetching the video and cover for a post |
/api |
prefix | the render pipeline, with the GATEWAY_API_TOKEN bearer token |
/healthz |
prefix | Kargo smoke test (the kubelet probes hit the pod directly) |
/, /privacy, /terms |
exact | policy pages registered on the platforms' app records |
/tiktok/callback, /facebook/callback |
exact | OAuth redirects for the consent trips |
/, /privacy and /terms must stay Exact. A / prefix would match every path and put
/admin and /metrics on the internet. A new route in the app needs a line here too. The
reelsmith-cloudflare-only middleware admits only Cloudflare's ranges, the same list as
trustedIPs in traefik/values.yaml, and uses
no ipStrategy because X-Forwarded-For can be spoofed. DNS for gate is in
cloudflare.md.
httproute-admin.yaml serves
reelsmith.local.bigd.no on the private gateway. / redirects to /admin/ with a 302,
/outpost.goauthentik.io/ goes to authentik-server in identity without auth (a
ReferenceGrant there allows it), and everything else goes through the reelsmith-authentik
forward-auth middleware. The Authentik side is
reelsmith-blueprint.yaml: a
forward_single proxy provider, and access limited to the group reelsmith-admins. Members are
declared in the blueprint, because reconciliation resets the list.
ciliumnetworkpolicy.yaml admits
port 8000 only from Traefik and Prometheus, plus the host and health entities for probes, and allows egress only to
CoreDNS and, on port 443, the publishing APIs: the Instagram and Facebook Graph
hosts, rupload.facebook.com, the Google API and OAuth hosts, and *.tiktokapis.com
(TikTok hands out its upload host per request). A new platform in the gateway needs
its host added there.
Configuration¶
Schedule and feature flags are in
configmap.yaml.
GATEWAY_SCHEDULER_ENABLEDis the master switch. Off, the gateway still answers comments but never publishes.GATEWAY_SLOTSholds one slot per line,HH:MM [zone] [jitter=N]plus the identity it posts for (account=<id>orbrand=<name>). Every line must name its identity: an unattributed line with several accounts registered, or a brand that matches no account, freezes the whole config sweep. A line that does not parse fails startup. Removing an account's last line removes its slots. Jitter is derived from slot and date, so a restart cannot publish twice.- The Facebook id is the Page id from
GET /me/accounts, not the number in the Page's URL. GATEWAY_ADMIN_TRUST_PROXY_AUTH: "true"means the app checks nothing itself on/admin. Authentik on the private route is the only lock, so/adminmust never be routed through any other gateway, and the network policy must keep port 8000 closed to other pods.GATEWAY_TIKTOK_ENABLEDalso gates the TikTok token refresher and insights loops. With it off, a queued TikTok post fails at its slot instead of retrying.GATEWAY_TIKTOK_PRIVACY_LEVELmust be one of the options TikTok'screator_inforeturns, or posts fail withprivacy_level_option_mismatch.GATEWAY_TIKTOK_DIRECT_POSTstaysfalsewhile the TikTok client is unaudited.
reelsmith-secret carries GATEWAY_APP_SECRET (signs Meta webhook deliveries),
GATEWAY_VERIFY_TOKEN (must match the value in the Meta dashboard) and GATEWAY_API_TOKEN (the
render pipeline's bearer token). The pod does not start without all three. Replacing the value in
Bitwarden rolls it within the hour: ESO refreshes hourly and Reloader restarts the pod.
State and backups¶
The SQLite database and the cover images are on reelsmith-state, a dynamic syno-nfs-csi PVC
with reclaim policy Delete. SQLite runs in WAL mode; if locking misbehaves on NFS, the fix is
the journal mode in the gateway's gateway/db.py, not a storage migration.
Reelsmith is not in VolSync. The gateway backs up its own database with VACUUM INTO:
/state/backupson the state volume, the newest 14 kept. These go with the PVC: the driver archives its directory on the NAS rather than deleting it, but a recreated claim starts empty (see storage)./offsite, a second copy on the statically bound PVreelsmith-offsite-backups(nas.local.bigd.no:/volume1/shared-data/media/reelsmith, reclaimRetain), mounted withsubPath: gateway-backups. On the NAS they are in/volume1/shared-data/media/reelsmith/gateway-backups.
The offsite volume is a PV and PVC, not an inline nfs volume, because the namespace enforces
Pod Security restricted, which rejects inline NFS volumes. The subPath keeps the render
pipeline's files beside it on the share out of the pod.
The answered-comments record in the database cannot be rebuilt from anywhere else. Restore steps are in restore.md.
Alerts¶
| Alert | Means | Action |
|---|---|---|
ReelsmithTokenExpiring, ReelsmithTokenAboutToDie |
an account token has under 10 or 2 days left | check the refresher in the logs; once expired it can only be re-authorised by hand in a browser, and publishing, DMs and insights stop for that account |
ReelsmithPollerStalled |
no comment poll sweep for over 10 minutes | comment-to-DM is off until it recovers |
ReelsmithSchedulerStalled |
no scheduler tick for over 10 minutes | a missed slot is not retried |
ReelsmithQueueStarved |
a slot came due with nothing approved | render and enqueue, or approve queued posts in the panel |
ReelsmithBackupStale, ReelsmithOffsiteBackupStale |
no backup, or no offsite copy, in 24 h | usually a full state volume, or the offsite mount gone |
ReelsmithRenderHostSilent |
no finished render run for a brand in 30 h | check /data/repos/reelsmith/build/nightly.log in verksted-app |
ReelsmithPublishFailing |
a platform rejected a publish in the last hour | the row stays queued and retries |
ReelsmithPostStuck |
a row sits in failed |
decide in the panel, see below |
ReelsmithClaimAbandoned |
a row sits in claimed past CLAIM_STALE_AFTER (1 h) |
decide in the panel, see below |
Logs for most of them:
kubectl -n reelsmith logs deploy/reelsmith-gateway | grep -i token # or backup, offsite, publish
Failed and abandoned posts¶
Neither state is self-healed, because the platform may already have accepted the post. A publish that failed after the container was created, or a process that died between container creation and publish, can leave a live Reel behind a row that looks unfinished.
- List the claimed rows (read-only, opens the database with
mode=ro, safe while the gateway runs):scripts/reelsmith-stale-claims.sh. - Check the account for the post.
- Resolve the row in the panel at
https://reelsmith.local.bigd.no/admin/. Re-arming a row whose post already went live publishes it twice.
A claimed row keeps its media exempt from the retention sweep until it is resolved. A failed
row with "no video file" means the retention sweep deleted a queued file.
Traps¶
- The Service carries the label
app: reelsmith-gatewayfor the ServiceMonitor. Without it Prometheus creates no target at all. - The network policy's Prometheus ingress rule is what lets
/metricsbe scraped; without it the target shows as down. GATEWAY_PUBLIC_BASE_URLis the externalhttps://gate.nordbye.it, because Meta fetches covers from it.- The Kargo Warehouse keeps
discoveryLimit: 10; old tags point at pruned manifests and a larger window fails discovery.