Prerequisites
Before you provision any instance, line up the accounts and shared resources every other step assumes are present. None of them is hard to create — but init.sh will fail loudly if anything in this list is missing.
Accounts and credentials
| Item | Why it's needed |
|---|---|
| Cloud provider account | Compute, networking, block storage. Hetzner Cloud is the reference; AWS EC2 is fully supported. Anything that runs Docker Compose on Linux works. |
| AWS account | SSM Parameter Store, Secrets Manager, S3 (config bundles + DB backups + recordings). Used regardless of which cloud hosts the compute. |
| AWS IAM user / role | Read SSM, read/write Secrets Manager, read S3, push and pull from your container registry, optionally call SES. |
| Container image registry | Reference setup uses AWS ECR. Any registry that supports docker login works as long as every host can reach it. |
| DNS control over two domains | One for the dashboard (DOMAIN_TELWEB), one for telephony (DOMAIN_TELPRO). Optional third for SigNoz UI (DOMAIN_SIGNOZ). |
| TLS sources | For dashboard / telephony — Let's Encrypt by default, or pre-issued certs in Secrets Manager. For internal Postgres/Redis — your own internal CA. |
| AI provider keys | OpenAI and/or Pythia/Vodafone TOBi/Azure, depending on which providers you enable in TelWeb after first boot. |
| (Optional) SIP carrier | A SIP trunk to point at TelPro's public IP. Without a carrier you can still test the stack via the WebRTC phone in TelWeb. |
Shared cloud resources
These resources are shared across every service in the deployment. Create them once per environment (staging, prod, etc.).
Namespace
Pick a NAMESPACE string of the form <org>/<environment>. Every SSM path and every Secrets Manager secret name is prefixed with it:
/voiceai/staging/api/REDIS_HOST
/voiceai/staging/database/INTERNAL_CA_CRT_B64
/voiceai/staging/api/DATABASE_SSL_MODE
voiceai/staging/api/secrets # JSON: DATABASE_URL, DATABASE_SSL_CA_BUNDLE_B64, REDIS_TLS_CA_BUNDLE_B64, …
voiceai/staging/web/secrets
The reference deployments use voiceai/staging and voiceai/production. If you operate multiple deployments under the same AWS account, use a unique NAMESPACE per deployment so the SSM paths and SM names never collide.
S3 config bucket
Every service host fetches its config bundle from a single S3 bucket at boot. The bucket layout is:
s3://<config-bucket>/
└─ deployments/<deployment-slug>/<release>/
├─ api/ docker-compose.yaml · init.sh · vars.yaml · otel-collector-config.yaml
├─ web/ docker-compose.yaml · init.sh · vars.yaml · Caddyfile · …
├─ voice/
├─ telpro/
├─ database/
├─ media/
├─ ops/
├─ signoz/
├─ squid/
└─ common/ fetch-env.sh · update.sh · prepare-tls.sh · …
The cloud-init on each host writes CONFIG_BUCKET and CONFIG_REF into /opt/deployment/.env; init.sh calls fetch-config to sync s3://${CONFIG_BUCKET}/deployments/<slug>/<CONFIG_REF>/<role>/ (and common/) into /opt/services/<role>/. Re-running update.sh --config-ref <new-ref> is how you roll a config change without rebuilding images.
The bundle itself is what you receive when a release is cut. The runtime path that consumes it is covered in Bootstrap and init.
Container images
Every release ships these images. Push them to your registry and remember the tag — it becomes ECR_TAG in the bootstrap environment:
| Image | Where it runs |
|---|---|
voiceai-telapi | API instances |
voiceai-telweb | Web instance |
voiceai-telphi | Voice instances |
voiceai-telsys | Voice instances (Asterisk) |
voiceai-telpro | TelPro instance (Kamailio with optional hiredis_ssl) |
voiceai-webrtc | TelPro instance (Janus) — only when the webrtc flag is on |
voiceai-turn | TelPro instance (coturn) — only when the webrtc flag is on |
voiceai-audioproc | Voice instances — only when the audioPreprocessing flag is on |
voiceai-media-server | Media instance — only when the ttsMediaCache flag is on |
voiceai-scaler | Ops instance |
voiceai-tasker | Ops instance |
voiceai-postgres | Database instance — wraps postgres:17-alpine |
voiceai-redis | Database instance — wraps redis:7-alpine |
voiceai-pgbouncer | Database instance — only when internal Postgres TLS is on |
voiceai-squid | Squid instance |
Upstream images (SigNoz, OTel collector, Caddy) are pulled directly from their public registries via Squid. Image-specific operational notes live with their service pages; WebRTC / Janus details are in TelPro operations.
Networks
- A private network (e.g.
10.0.1.0/24in the reference) shared by every service instance. - A reserved fixed private IP for SigNoz (
10.0.1.10by default) — every OTel collector points at this address. - A reserved fixed private IP for Media (
10.0.1.30by default) — used asTTS_MEDIA_CACHE_BASE_URLhost. - Static / floating public IPs for TelPro and Web so DNS records can be set without rotating after instance recreation.
- A managed load balancer in front of the API instances, with TLS termination on
DOMAIN_APIand sticky sessions enabled (required for WebSocket).
IAM grants
Each instance role needs:
- SSM —
ssm:GetParametersByPathunder/${NAMESPACE}/${SERVICE}/. - Secrets Manager —
secretsmanager:ListSecretsfiltered by name prefix${NAMESPACE}/${SERVICE}/, andsecretsmanager:GetSecretValuefor each match. Plus any external secrets referenced bysecret.arn_from(e.g. an RDS master secret). - S3 —
s3:GetObject,s3:ListBucketunders3://<config-bucket>/deployments/<slug>/. - ECR —
ecr:GetAuthorizationToken,ecr:BatchGetImage,ecr:GetDownloadUrlForLayerfor the registry you pushed images to. - (Ops only) Permissions to drive the cloud-provider scaling API: Hetzner API token in Secrets Manager, or AWS EC2 RunInstances if scaling on AWS. SES
ses:SendEmail/ses:SendRawEmailif email goes through SES. - (Database only, if you use a managed RDS / Aurora cluster)
secretsmanager:GetSecretValueon the RDS master secret ARN — see Managed database secrets.
Operator tooling
On the workstation you bring the deployment up from:
awsCLI v2 (configured for the deployment's AWS account).- Your provisioning tool of choice for creating instances, volumes, firewalls / security groups, and load balancers.
ssh,scp(Bastion access).yq,jq(used byfetch-env.shand many of the helper scripts on the host as well).
Next
Continue to Configure environment.