Self-hosting Tiep
This guide deploys the clinician app, administrator platform, API, Redis worker, and the infrastructure they need. It is intended for an operator who owns the domains, Firebase project, Langfuse project, and AI-provider credentials.
There are two supported full-stack options:
| Option | Best for | You operate |
|---|---|---|
| Bundled infrastructure | A single server or evaluation environment | Docker runs PostgreSQL/pgvector, Redis, and MinIO in persistent volumes. |
| External infrastructure | Production environments with managed services | You provide PostgreSQL/pgvector, Redis, and S3-compatible object storage. |
Frontend-only deployments are not covered here. They require an independently operated compatible API.
What you need
Before starting, have the following ready:
- A Linux host with Docker Engine and the Docker Compose plugin. The Docker daemon must be running.
- Three DNS names:
app.<domain>,platform.<domain>, andapi.<domain>. - A reverse proxy that terminates TLS and can reach loopback ports on the host.
- A customer-owned Firebase project, configured for browser authentication.
- A customer-owned Langfuse project and its API keys.
- An OpenAI or Groq API key for report generation, and a Groq or Fal key for transcription.
- For the external option, PostgreSQL with pgvector support, Redis, and an S3-compatible bucket.
The deployment listens only on the host loopback interface:
| Service | Host address | Public DNS name |
|---|---|---|
| Clinician app | 127.0.0.1:3001 | app.<domain> |
| Administrator platform | 127.0.0.1:3002 | platform.<domain> |
| API | 127.0.0.1:8080 | api.<domain> |
Configure the reverse proxy to route each hostname to its matching port, terminate TLS, and preserve standard forwarded request headers. Do not publish these ports directly to the internet.
Architecture
Choose one infrastructure option:
With the bundled option, PostgreSQL, Redis, and MinIO run as Docker services on this host. With the external option, the same containers connect to operator-managed equivalents.
Component descriptions
| Component | Responsibility |
|---|---|
| Clinician app | The SvelteKit web application used by clinicians. It receives public Firebase browser configuration and sends authenticated requests to the API. |
| Administrator platform | The SvelteKit web application used to administer organizations, users, plans, and forms. It shares the same API and Firebase project. |
| Reverse proxy | The operator-managed entry point. It owns DNS and TLS, routes the three public hostnames to loopback ports, and should restrict operator-only diagnostic endpoints. |
| API | The FastAPI service. It verifies Firebase bearer tokens, applies authorization and CORS, serves application requests, stores task records, and queues background work. |
| Redis worker | A separate process that consumes Redis Stream tasks, performs long-running work such as transcription, retries failed tasks, and updates task status in PostgreSQL. |
| PostgreSQL with pgvector | The system-of-record database for organizations, users, forms, consultations, reports, tasks, and task outbox records. pgvector support is required by the application schema. |
| Redis Streams | The durable task-dispatch channel between the API and worker. Consumer-group acknowledgments and recovery handle interrupted worker deliveries. |
| S3-compatible storage | Stores uploaded consultation audio. Bundled mode uses MinIO; external mode uses an operator-managed compatible bucket. |
| Customer Firebase | Provides browser authentication and issues bearer tokens. The backend service account verifies tokens and manages Firebase users and custom claims. |
| Customer Langfuse | Stores and resolves the versioned AI prompts and receives tracing data. It must contain the bootstrapped prompt templates before AI requests succeed. |
| OpenAI, Groq, or Fal | Operator-selected BYOK AI services. OpenAI or Groq generates reports; Groq or Fal transcribes audio. Prompts, or audio for transcription, are sent to the selected provider. |
Prepare Firebase
Tiep uses Firebase Authentication for identity. The browser receives the Firebase web configuration; the API uses a separate, private service-account JSON file.
-
In Firebase Console, open the customer project and enable the sign-in provider used for email-link login under Authentication → Sign-in method.
-
Under Authentication → Settings → Authorized domains, add
app.<domain>andplatform.<domain>. Add local development hostnames only when you intend to use them. -
Under Project settings → General → Your apps, create or select a Web app and copy its Firebase configuration. It supplies the
PUBLIC_FIREBASE_*variables below. -
Under Project settings → Service accounts, generate a new private key. Save it as
firebase-service-account.jsonin this repository's root directory:tiep/├── firebase-service-account.json├── .env.self-hosted└── docker-compose.self-hosted.yamlThis file is mounted only into the API, worker, and migration containers. It must belong to the same Firebase project as
PUBLIC_FIREBASE_PROJECT_IDand must never be committed or placed in a frontend container.
Configure the environment
Copy the tracked template and edit only the copy:
cp .env.self-hosted.example .env.self-hosted
chmod 600 .env.self-hosted firebase-service-account.json
.env.self-hosted is ignored by Git. Its values are supplied to all relevant containers at runtime.
Public URLs and Firebase web configuration
Set these values in every deployment. The frontend containers fail at startup if required public values are missing.
| Variable | Value |
|---|---|
PUBLIC_API_URL | Public API URL, for example https://api.example.com |
PUBLIC_APP_URL | Public clinician app URL, for example https://app.example.com |
PUBLIC_PLATFORM_URL | Public administrator platform URL, for example https://platform.example.com |
PUBLIC_APP_ENVIRONMENT | Usually production; use development only for a non-production instance |
PUBLIC_FIREBASE_API_KEY through PUBLIC_FIREBASE_APP_ID | Values from the Firebase Web app configuration |
PUBLIC_FIREBASE_MEASUREMENT_ID | Optional Firebase Analytics measurement ID |
PUBLIC_FIREBASE_PROJECT_ID | Firebase project ID; must match FIREBASE_PROJECT_ID |
CORS_ALLOWED_ORIGINS | Comma-separated public frontend origins, for example https://app.example.com,https://platform.example.com |
Firebase's web API key is browser configuration, not a service-account secret. Do not put the service-account JSON contents in any PUBLIC_* variable.
Backend, storage, and task settings
These settings select the self-hosted execution path:
| Variable | Required value |
|---|---|
DEPLOYMENT_MODE | self_hosted |
TASK_DISPATCHER | redis |
FIREBASE_PROJECT_ID | The customer Firebase project ID |
FIREBASE_CREDENTIALS_FILE | /run/secrets/firebase-service-account.json |
PATIENT_FIELD_ENCRYPTION_KEY | Secret symmetric key used to encrypt sensitive patient fields stored in PostgreSQL |
DATABASE_URL | Async PostgreSQL URL, such as postgresql+asyncpg://... |
REDIS_URL | Redis URL, such as redis://.../0 |
S3_ENDPOINT_URL, S3_REGION, S3_BUCKET | Reachable S3-compatible object storage details |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Bucket credentials |
TASK_MAX_ATTEMPTS | Optional positive retry limit; defaults to 3 |
TASK_VISIBILITY_TIMEOUT_SECONDS | Optional Redis message recovery timeout; defaults to 900 |
Audio is stored under sessions/<session-id>/<timestamp> in the configured S3 bucket. PostgreSQL tracks task state and Redis Streams dispatches background work to the worker.
Langfuse and AI credentials
Langfuse is required for prompt resolution and tracing. Set LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, and LANGFUSE_SECRET_KEY for one customer Langfuse project.
For AI, choose one default generation provider and a matching supported model:
| Provider | Key | Supported models |
|---|---|---|
openai | OPENAI_API_KEY | GPT_5_MINI, gpt-4.1-2025-04-14, gpt-5.4-mini-2026-03-17, gpt-5.4-2026-03-05, gpt-5.5-2026-04-23 |
groq | GROQ_API_KEY | llama-3.1-8b-instant, llama-3.3-70b-versatile, openai/gpt-oss-120b |
Set AI_DEFAULT_PROVIDER and AI_DEFAULT_MODEL to one row above. Also configure at least one transcription provider: GROQ_API_KEY or FAL_KEY. Vertex AI is unavailable in self-hosted mode.
Prompts and traces are sent to the configured Langfuse host. Audio sent to Groq or Fal for transcription leaves the deployment.
Optional observability and integrations
Backend SendGrid, PostHog, Logfire, Sentry, and Intercom integrations are disabled when their variables are omitted. The self-hosted clinician app also leaves Sentry, PostHog, and Logfire uninitialized unless explicitly enabled with browser-facing configuration:
| Variable | Effect |
|---|---|
PUBLIC_SENTRY_DSN | Initializes browser Sentry with this public DSN. |
PUBLIC_POSTHOG_API_KEY | Initializes browser PostHog with this public project key. PUBLIC_POSTHOG_HOST is optional and defaults to https://eu.i.posthog.com. |
PUBLIC_LOGFIRE_ENABLED=true | Initializes browser Logfire and proxies its telemetry through the API. Also set the private API LOGFIRE_TOKEN; never expose that token to the browser. |
Leave these frontend variables empty to disable their browser SDKs. The administrator platform does not currently initialize these SDKs.
For secrets managed as files, mount the file into the relevant container and set the matching *_FILE variable, for example OPENAI_API_KEY_FILE=/run/secrets/openai_api_key. Do not commit secrets or operator-specific configuration.
Option A: Bundled infrastructure
Use this option when one Docker host should run the entire stack. Docker creates persistent volumes for PostgreSQL, Redis, and MinIO.
-
Complete Prepare Firebase and Configure the environment.
-
Keep the bundled values from
.env.self-hosted.exampleforDATABASE_URL,REDIS_URL, andS3_ENDPOINT_URL. Set strong, unique values forPOSTGRES_PASSWORD,S3_ACCESS_KEY_ID, andS3_SECRET_ACCESS_KEY. -
Confirm Compose resolves the configuration before starting anything:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \--profile bundled config -
Build and start the stack:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \--profile bundled up --build -d --waitmigrateexits successfully after applying database migrations.minio-initalso exits successfully after creating the bucket; that is expected. -
Bootstrap the four repository-managed Langfuse prompts:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \run --rm api /app/.venv/bin/python -m commands.bootstrap_langfuse_prompts -
Verify storage and service readiness:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \run --rm api /app/.venv/bin/python -m commands.verify_storagecurl -fsS http://127.0.0.1:8080/_internal/health/readycurl -fsS http://127.0.0.1:8080/_internal/health/deep -
Sign in once through the clinician or platform URL, then assign the first superadmin:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \run --rm -it api /app/.venv/bin/python -m commands.createsuperuser
Option B: External infrastructure
Use this option when PostgreSQL, Redis, and object storage are operated outside this Compose project.
-
Provision infrastructure that the Docker host can reach:
- PostgreSQL with pgvector support, a database, and an application user able to run migrations.
- Redis reachable by both the API and worker.
- An S3-compatible bucket with permissions to list the bucket and create, read, and delete objects.
-
Complete Prepare Firebase and Configure the environment.
-
Replace the bundled settings in
.env.self-hostedwith operator-managed endpoints and credentials. Use apostgresql+asyncpg://DATABASE_URL, a RedisREDIS_URL, and an absolutehttp://orhttps://S3 endpoint URL. Prefer encrypted connections when services are outside the host network. -
Validate the configuration without the bundled profile:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml config -
Start the application services without
--profile bundled:docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \up --build -d --wait -
Run the Langfuse bootstrap, storage verification, readiness checks, and initial superadmin assignment from steps 5–7 of the bundled guide.
You own the availability, encryption, backup, restoration, upgrades, and access control of external PostgreSQL, Redis, and S3-compatible storage.
Day-two operations
Health and logs
| Endpoint | Purpose |
|---|---|
/_internal/health/live | Process liveness only |
/_internal/health/ready | Configuration, PostgreSQL, and Redis readiness; used by Docker health checks |
/_internal/health/deep | On-demand S3, Firebase, and Langfuse diagnostic checks with timeouts |
Use the API's loopback address locally or route these paths through an authenticated operator-only proxy location. Do not expose deep diagnostics publicly.
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml logs -f api worker
Upgrade
Back up first. Then pull or check out the intended version and rebuild the services. The migrate service runs before the API and worker start; to run migrations manually:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml run --rm migrate
After updating prompt templates, rerun the Langfuse bootstrap command. It creates changed prompts and leaves unchanged templates alone.
Backup and restore for bundled infrastructure
Keep backups outside the Docker host and test restoration on a replacement deployment.
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T postgres pg_dump -U tiep tiep > tiep.sql
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T minio sh -c 'tar -C /data -cf - .' > minio-data.tar
Restore only to a stopped or replacement deployment after verifying the backup:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T postgres psql -U tiep -d tiep < tiep.sql
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T minio sh -c 'tar -C /data -xf -' < minio-data.tar
For external infrastructure, use the backup and restore procedures supplied by its operator.
Stop the deployment
Stop containers while preserving bundled PostgreSQL, Redis, and MinIO data:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled down
Delete containers and all bundled persistent data only when intentionally resetting the instance:
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled down --volumes