Media service operations
The Media service is a private HTTPS cache for generated TTS audio. Caddy (caddy:2-alpine) serves files on GET /media/...; media-upload (voiceai-media-server, built from Go 1.24-alpine onto alpine:3.19) handles authenticated PUT / DELETE /media/.... Data lives on a volume mounted at MEDIA_DATA_DIR (default /mnt/media-data). Static private IP 10.0.1.30.
- Overview
- Runbook
- Configuration
- Troubleshooting
Instance
Instance vCPU and RAM are in Provision instances.
| Field | Value |
|---|---|
| Network | Private only — static private IP 10.0.1.30 |
| Block storage | Attached volume at MEDIA_DATA_DIR (default /mnt/media-data) |
| Scalable | No — single instance |
Endpoints
| Method | Path | Behavior |
|---|---|---|
GET / HEAD | https://<host>/media/<shard>/<hash>.alaw|pcm | Caddy file_server from /data/media/.... Supports HTTP Range / 206. |
PUT | https://<host>/media/... | Reverse-proxied to media-upload. Optional Authorization: Bearer <MEDIA_UPLOAD_TOKEN>. |
DELETE | https://<host>/media/... | Same auth as PUT. Returns 404 if missing. |
GET | https://<host>/health | Returns ok. |
Consumers
TelAPI and TelPhi read and write the cache via TTS_MEDIA_CACHE_BASE_URL, TTS_MEDIA_CACHE_UPLOAD_TOKEN, and TTS_MEDIA_CACHE_CA_BUNDLE.
Database metadata
Logical cache rows live in Postgres as TtsMediaCache (tts_media_cache): hash, sentence, voice, model, speed, variants (JSON array of encodings), optional ttl. Bytes stay on the media volume; Tasker can delete blobs via HTTP DELETE and drop rows by ttl.
Capacity
For typical TTS clip sizes and ~1000 concurrent HTTP connections, the reference Media instance size with SSD and raised nofile ulimits (set in the compose file) is usually sufficient. If Caddy CPU or disk I/O saturates, add another media node behind an internal load balancer and shard cache keys — avoid NFS for hot paths if latency matters.
TLS sources
This host typically has no outbound internet, so Let's Encrypt is not used. init.sh resolves TLS in this order:
- PEM from env — if
MEDIA_TLS_FULLCHAIN_B64andMEDIA_TLS_PRIVKEY_B64are set (base64-encoded PEM), they are written tofullchain.pem/privkey.pem. - Existing files — if
tls/fullchain.pemandtls/privkey.pemalready exist underMEDIA_TLS_CERT_HOST_PATH, they are used as-is. - Self-signed — otherwise OpenSSL generates a cert with SAN
IP:${PRIVATE_IP}(and optionalDNS:${MEDIA_TLS_SAN_DNS}).
After self-signed generation, tls/ca-for-clients.pem is a copy of fullchain.pem — TelPhi / TelAPI must trust this PEM via TTS_MEDIA_CACHE_CA_BUNDLE.
Distributing trust material to consumers
Docker Compose only passes one line per env var into YAML; multiline PEM stored in SSM gets truncated. The recommended pattern is base64-encoded PEM in a single line:
base64 -w0 /opt/services/media/tls/ca-for-clients.pem
# macOS:
base64 -i ca-for-clients.pem | tr -d '\n'
Store the result in:
/voiceai/<env>/voice/TTS_MEDIA_CACHE_CA_BUNDLE/voiceai/<env>/api/TTS_MEDIA_CACHE_CA_BUNDLE
TelPhi and TelAPI decode the bundle in memory at process start — PEM never lands on disk in the consumer.
The helper common/check-tts-media-cache-tls.sh --docker voiceai-telapi reproduces the consumer-side TLS stack and runs openssl s_client plus curl --cacert. Use it whenever you change CA material.
Capacity planning (~1000 concurrent calls)
- Caddy
file_serveruses sendfile; the Linux page cache holds hot objects in RAM. - For typical TTS clip sizes and 1000 concurrent HTTP connections, a single
cpx22-class node with SSD and raisednofileulimits is usually enough. - Bottlenecks to watch:
ulimit -n, Docker defaults, network bandwidth between voice/api and media. - Scale out by adding more media nodes behind an internal LB and sharding by hash prefix; avoid NFS for the hot path.
| Name | Source | Scope | Default | Description |
|---|---|---|---|---|
MEDIA_DATA_DIR | SSM | all | /mnt/media-data | Attached volume mount point. |
MEDIA_UPLOAD_TOKEN | Secrets Manager | all | — | Bearer token required for PUT / DELETE writes. |
MEDIA_TLS_FULLCHAIN_B64 | SSM | all | — | Base64 PEM fullchain (preferred TLS source). |
MEDIA_TLS_PRIVKEY_B64 | Secrets Manager | all | — | Base64 PEM private key. |
MEDIA_TLS_CERT_HOST_PATH | SSM | all | — | Optional override for the cert host path. |
MEDIA_TLS_SAN_DNS | SSM | all | — | Optional DNS SAN for self-signed generation. |
Wiring TLS material from AWS
| Variable | Store | Path |
|---|---|---|
MEDIA_TLS_FULLCHAIN_B64 | SSM Parameter Store | /${NAMESPACE}/media/MEDIA_TLS_FULLCHAIN_B64 (String, base64 of PEM, single line) |
MEDIA_TLS_PRIVKEY_B64 | Secrets Manager | ${NAMESPACE}/media/secrets JSON field MEDIA_TLS_PRIVKEY_B64 (shares the secret with MEDIA_UPLOAD_TOKEN) |
Both variables must be present and non-empty for branch (1); otherwise init falls through to existing files or self-signed.
| Symptom | Likely cause | Check |
|---|---|---|
TelPhi / TelAPI DEPTH_ZERO_SELF_SIGNED_CERT | CA bundle missing on consumer | TelAPI startup tlsDiag (mediaCacheTlsMode, decodedCaBundleByteLength); run common/check-tts-media-cache-tls.sh --docker voiceai-telapi. |
| Uploads return 401 | MEDIA_UPLOAD_TOKEN drift | Verify the secret matches across consumers. |
bad end line from curl --cacert | Single-long-line base64 PEM | Re-encode as 64-char-wrapped PEM (the helper script does this automatically). |
| Disk full | Cache TTL not running | Verify Tasker MediaCacheCleanup job; manual DELETE of stale entries. |
| One-way TLS failure after cert rotation | Consumer CA bundle outdated | Sync new CA into SSM and roll consumers. |
See also
- API operations — TelAPI reads and writes the cache.
- Voice operations — TelPhi reads and writes the cache.
- Ops operations — Tasker drives the cleanup job.