Files
feedc0de 9fd543ddcb
Validate and deploy asciinema / Validate manifests (push) Successful in 13s
Validate and deploy asciinema / Deploy to Kubernetes (push) Successful in 1m6s
Fix asciinema S3 deployment
2026-10-04 17:17:46 +02:00

4.9 KiB

asciinema on Kubernetes

Production deployment for https://asciinema.brunner.ninja in the asciinema namespace.

Durable data and dependencies

asciinema has two durable data sets:

  • A dedicated PostgreSQL 17 StatefulSet stores users, recording metadata, searchable terminal text, and stream state. Its 8 Gi RBD claim uses the retained rook-ceph-block-ec StorageClass.
  • The private Rook Ceph RGW bucket asciinema stores uploaded asciicast files. The server reads casts through the cluster-local RGW Service and serves them from the asciinema endpoint.

All S3 operations use the cluster-local RGW Service. Although the S3 adapter generates a pre-signed URL when reading a recording, asciinema consumes that URL server-side to populate its local cache; it is not sent to the browser. Keeping the URL internal also avoids routing storage reads through Traefik.

The application needs neither Redis nor Valkey. Its cache and temporary upload workspace are bounded emptyDir volumes and may be discarded when the pod moves. PostgreSQL's dedicated PVC and the RGW pool are the durable storage and both must be backed up.

A PVC mounted at /var/lib/asciinema would also work instead of S3, but it would couple the pod to a filesystem volume and make future replicas and file serving less flexible. Since this cluster already operates RGW, S3 is the simpler production fit.

Install

Run with the administrator kubeconfig for this cluster:

./install.sh

The script creates and preserves random database, Phoenix signing, and Erlang cookie values in asciinema/asciinema-secrets. It provisions a dedicated Rook object user, copies only that user's generated credentials into the application namespace, initializes the dedicated PostgreSQL instance and RGW bucket, then applies and verifies the application.

No secret values are committed or printed. Re-running the script preserves the application secrets and reconciles the database password and Rook credentials.

asciinema.brunner.ninja is a CNAME to brunner.ninja, and the existing wildcard certificate covers the hostname.

First login and hardening

Sign-ups are initially enabled so the first account can be created; upstream makes that first account an administrator. Login links are sent through the existing smtp-relay.gmail.com relay. If mail delivery is unavailable, find the generated one-time URL in the application logs:

kubectl logs deployment/asciinema -n asciinema | grep 'url from email'

After creating the account, set SIGN_UP_DISABLED to "true" in asciinema.yaml, apply it, and wait for the rollout. CLI uploads already require authentication, and new recordings and streams default to unlisted. The login-gated admin panel is available at /admin; the unauthenticated dedicated admin port is not exposed by the Service.

Configure asciinema CLI 3.x with:

export ASCIINEMA_SERVER_URL=https://asciinema.brunner.ninja
asciinema auth

For asciinema CLI 2.x, use ASCIINEMA_API_URL instead.

Validate

./test.sh

The test checks the scripts, enforces the pinned server image and storage architecture, and validates all Kubernetes resources with kubeconform.

Gitea Actions deployment

The workflow in .gitea/workflows/deploy.yml validates every push and pull request. A successful push to main performs server-side dry runs, reconciles the dedicated PostgreSQL Service and StatefulSet plus the asciinema ConfigMaps, Deployment, Service, and Ingress, waits for both workloads, and checks the public HTTPS endpoint.

CI deliberately does not run install.sh or apply namespace.yaml, bootstrap.yaml, or ceph-object-user.yaml. Those administrator-only resources create credentials, storage, the bucket, and the initial workload. CI cannot read Secrets, create Jobs, change PVCs, modify the Namespace, or access Rook. If a managed runtime resource is deleted, CI fails instead of recreating it; restore it with ./install.sh.

Bootstrap the restricted deployment identity once:

./create-ci-kubeconfig.sh

For remote cluster access through the usual SSH host:

KUBECTL_SSH_HOST=arschrock ./create-ci-kubeconfig.sh

Store the script's single base64 output line as the Gitea repository Actions secret KUBE_CONFIG_BASE64. Protect main, require the validation job before merging, and push a harmless change to verify the first deployment.

Delete asciinema/asciinema-deployer-token and the Gitea Actions secret to revoke CI access. Running create-ci-kubeconfig.sh again recreates the token.

The Role is intentionally name-scoped, but Kubernetes RBAC cannot constrain individual fields of a Deployment or StatefulSet patch. A malicious change merged into main could alter the pod specifications and expose Secrets already referenced by those workloads. Branch protection and review remain part of the security boundary; admission policy would be needed for field-level controls.