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-ecStorageClass. - The private Rook Ceph RGW bucket
asciinemastores uploaded asciicast files. Downloads use expiring pre-signed URLs throughhttps://s3.brunner.ninja.
The public RGW ingress rejects ExAws signatures for some mutating methods, so
custom.exs uses the cluster-local RGW Service for server-side writes, moves,
and deletes while generating browser-visible pre-signed GET URLs with the
public endpoint. This mirrors the internal/public endpoint split used by other
S3-backed applications in the cluster.
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.