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
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.
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.