feedc0de 9d0e7f5c95
Validate and deploy asciinema / Validate manifests (push) Successful in 13s
Validate and deploy asciinema / Deploy to Kubernetes (push) Successful in 36s
First try in deploying asciinema to kubernetes
2026-09-11 09:10:24 +02:00
2026-09-11 08:55:35 +02:00

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. Downloads use expiring pre-signed URLs through https://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.

S
Description
Deployment files for asciinema in my cluster
Readme GPL-3.0
63 KiB
Languages
Shell 100%