Skip to content

Tusd

tusd handles resumable/chunked file uploads (the tus protocol) and streams them directly to SeaweedFS — no local disk writes. On completion, it notifies coreapi via a webhook. Assumes SeaweedFS and the playtelly bucket already exist.

Overview

  • 2 replicas, autoscaling 2–6 via HPA on CPU (70%) / memory (80%)
  • Streams uploads straight to the internal SeaweedFS S3 endpoint, bucket playtelly
  • Calls coreapi's webhook on upload completion (post-finish hook)
  • Requires sticky sessions — tusd's default locker tracks upload locks in-memory per pod

Architecture

Client (browser)
   ↓ resumable upload (tus protocol)
tusd Service (sessionAffinity: ClientIP, 3hr timeout)
   ↓
tusd pods (2–6, HPA)
   ↓ streams directly to S3, no local writes
SeaweedFS S3 gateway (seaweedfs-s3.seaweedfs.svc.cluster.local:8333) → playtelly bucket
   ↓ on completion (post-finish hook)
coreapi-service.production.svc.cluster.local:3000/hooks/tusd

Prerequisites

  • SeaweedFS running with the playtelly bucket already created (see SeaweedFS)
  • coreapi reachable at coreapi-service.production.svc.cluster.local:3000, with /hooks/tusd implemented
  • metrics-server running in-cluster — required for the HPA to read CPU/memory metrics; without it the HPA silently can't scale

Files to apply

File Purpose
01-configmap.yaml tusd-config — non-secret env vars
02-secret.yaml tusd-s3-credentials — S3 access key/secret
03-deployment.yaml Main Deployment — 2 replicas, anti-affinity, rolling update
04-service.yaml ClusterIP Service with sticky sessions
05-pdb.yaml PodDisruptionBudget — minAvailable: 1
06-hpa.yaml HorizontalPodAutoscaler — 2–6 replicas

02-secret.yaml (example — replace the values):

apiVersion: v1
kind: Secret
metadata:
  name: tusd-s3-credentials
  namespace: production
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "admin"        # override with your actual key
  AWS_SECRET_ACCESS_KEY: "password" # override with your actual secret

⚠️ Must configure before applying

What Where Why it matters
Image tag 03-deployment.yamlimage: Pin an exact version (e.g. v2.4.0) — a floating tag like v2 means the running version can silently change on the next pod restart
-s3-bucket 03-deployment.yaml → args Must match the real SeaweedFS bucket name (playtelly)
-s3-endpoint 03-deployment.yaml → args Must point at the internal SeaweedFS S3 service, e.g. http://seaweedfs-s3.seaweedfs.svc.cluster.local:8333
-hooks-http 03-deployment.yaml → args Must point at the real coreapi service/endpoint that handles /hooks/tusd
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY 02-secret.yaml Real SeaweedFS identity credentials — not the placeholder admin/password shown above
-cors-allow-origin=.* 03-deployment.yaml → args Wildcard is fine to get running, but lock this down to the real frontend domain before this is truly production-ready
tusd-config / tusd-s3-credentials names 01-configmap.yaml / 02-secret.yaml The Deployment's envFrom references these by exact name with no -production suffix — confirm they exist under those exact names in the production namespace before applying

⚠️ Do not change these — required exactly as documented

Setting Why it can't be adjusted casually
runAsUser: 1000 / runAsGroup: 1000 in securityContext The tusproject/tusd image creates its non-root user by name (tusd), not a numeric UID Kubernetes can resolve at admission time. Without an explicit numeric UID, kubelet can't verify non-root and refuses to start the container (CreateContainerConfigError)
sessionAffinity: ClientIP on the Service tusd's default locker tracks upload locks in-memory, per pod. Without sticky sessions, a client's PATCH sequence for one upload could hit different pods and corrupt it
rollingUpdate.maxUnavailable: 0 / maxSurge: 1 Zero-downtime deploys — dropping this can briefly take capacity below what's needed mid-rollout
PodDisruptionBudget minAvailable: 1 Protects against a node drain or rolling update taking down all replicas simultaneously
HPA's slow scale-down (5 min stabilization window) Reduces (but doesn't eliminate — see caveat below) the chance of killing a pod mid-upload during scale-down
readOnlyRootFilesystem: true Only safe because tusd streams uploads directly to S3 with no local writes. If tusd's behavior ever changes (temp files, hook side effects writing locally), this will need revisiting — it hasn't been stress-tested against that

⚠️ Known tension — HPA scale-down vs. sticky sessions

Sticky sessions and autoscaling pull in opposite directions: if a pod handling an in-progress upload gets removed during scale-down, that upload fails regardless of how PDB/stabilization windows are tuned — those reduce the chance, not eliminate it. If large uploads become common, a distributed locker (e.g. etcd-backed) instead of tusd's in-memory default would remove this tension entirely, at the cost of extra infrastructure.

Deploy

kubectl apply -f tusd/prod/01-configmap.yaml
kubectl apply -f tusd/prod/02-secret.yaml
kubectl apply -f tusd/prod/03-deployment.yaml
kubectl apply -f tusd/prod/04-service.yaml
kubectl apply -f tusd/prod/05-pdb.yaml
kubectl apply -f tusd/prod/06-hpa.yaml

kubectl rollout status deployment/tusd -n production
kubectl get pods -n production -l app=tusd

Verify

# Confirm which bucket/endpoint the running Deployment is actually using
kubectl get deployment tusd -n production -o jsonpath='{.spec.template.spec.containers[0].args}'

kubectl get hpa -n production
kubectl get pdb -n production

Troubleshooting

Symptom Cause Fix
CreateContainerConfigError: "container has runAsNonRoot and image has non-numeric user" runAsNonRoot: true set without a numeric runAsUser Add runAsUser: 1000 / runAsGroup: 1000 (the image's built-in tusd user)
Upload corrupts partway through, especially under load Sticky sessions missing, or an Ingress/LB in front of the Service isn't honoring the Service's sessionAffinity Confirm sessionAffinity: ClientIP on 04-service.yaml; if there's an Ingress/LB in front, it needs its own separate sticky-session config — the Service-level setting alone doesn't cover that layer
HPA shows <unknown> for CPU/memory targets, never scales metrics-server not running in-cluster Install/verify metrics-server

One-off ops note — renaming the S3 bucket later

If the bucket ever needs to be renamed (as happened once already, cmsplaytelly), the general pattern is: check the old bucket for real data, delete any leftover test objects, delete the now-empty bucket, re-run the SeaweedFS init Job with the new bucket name, then update tusd's -s3-bucket arg and re-apply. Not part of a fresh install — noted here only because it's happened once and will likely happen again.