Files
triggerdotdev--trigger.dev/docs/self-hosting/docker.mdx
T
Matt Aitken cb9aefd49b fix(hosting): deploy ClickHouse from the official image instead of Bitnami (#4249)
## Summary

Self-hosted deployments now run ClickHouse from the official
[`clickhouse/clickhouse-server`](https://hub.docker.com/r/clickhouse/clickhouse-server)
image instead of `bitnamilegacy/clickhouse`. Bitnami's free image
catalog is EOL and the frozen legacy archive tops out at ClickHouse
25.7.5, below the 25.8 minimum the platform requires since v4.5.0, which
broke every ClickHouse insert on chart-bundled deployments. Both stacks
now default to 26.2, the same version the platform is developed and
tested against.

Existing deployments keep their ClickHouse data with no manual
migration.

Fixes #4197.

## Details

**Docker Compose**: the `clickhouse` service uses the official image
with its native env vars, plus the recommended `nofile` ulimits. It
reuses the same named volume as before: a `data-paths.xml` config
override points ClickHouse at the `data/` subdirectory of the volume,
which is exactly the layout the Bitnami image used, so old volumes work
in place (including SQL-created users) and fresh installs get the
identical layout. The service follows the required-secrets model:
`CLICKHOUSE_PASSWORD` must be set, matching the other services.

**Helm chart**: the Bitnami ClickHouse subchart is replaced by a
chart-owned single-node StatefulSet and Service running the official
image (non-root, HTTP `/ping` probes, config overrides mounted into
`config.d`, and the same `data-paths.xml` layout compatibility). On
upgrade, the chart automatically adopts the data PVC left behind by the
old subchart (`data-<release>-clickhouse-shard0-0`) via `lookup`, and
`fsGroup` relabeling handles the uid change on first mount. Both the
ClickHouse server and the webapp read the password from the same
chart-managed datastore secret (auto-generated and retained across
upgrades), so the server credential and the app's connection URL always
match. Existing `clickhouse.*` values keep working: `auth` (including
`existingSecret`/`existingSecretKey`), `persistence` (including
`global.storageClass`), `resources`, `secure`, `external.*`,
`configdFiles`, and now `nodeSelector`/`tolerations`/`affinity`.
Bitnami-only keys (`shards`, `replicaCount`, `keeper`,
`resourcesPreset`) are gone; default `resources` requests/limits match
what the old preset applied. The docs state the 25.8 minimum for
bring-your-own ClickHouse.

## Upgrade caveats

An adversarial review of the upgrade path found a few cohorts that need
awareness (all documented):

- **GitOps tools that render with `helm template`** (no cluster access):
PVC auto-detection can't run, so `clickhouse.persistence.existingClaim`
must be set to the old PVC name or ClickHouse starts on a fresh empty
volume. Documented in the values file and the Kubernetes self-hosting
docs. Tools that run real helm installs (e.g. Flux) adopt automatically.
- **A pinned `CLICKHOUSE_IMAGE_TAG`** pointing at a Bitnami tag must be
updated to an official image tag; documented in the Docker self-hosting
docs.
- **Storage without `fsGroup` support** (NFS, hostPath): set
`clickhouse.volumePermissions.enabled: true` for a one-time
ownership-fixing init container.
- **Rollback is not automatic**: once the official image has run, file
ownership changes and the Bitnami image can no longer read the volume
without a manual chown, and ClickHouse does not support downgrades
across the version gap.

## Verification

- Full upgrade simulation for Compose, twice (before and after rebasing
onto the required-secrets release): booted the ClickHouse service from
the old compose file on `main` (Bitnami), wrote thousands of rows, then
brought the same project up with this branch's compose file. The
official 26.2 server came up healthy on the same volume with all rows
intact, SQL-created users working, and writes succeeding.
- Adoption scenarios tested against real containers: old volume + root
entrypoint (Compose), old volume owned by the Bitnami uid + non-root 101
with fsGroup-style group permissions (Kubernetes), and fresh volumes for
both.
- `helm lint`, `helm template` (default values, `existingClaim` set,
external ClickHouse, volumePermissions/scheduling toggles, and the
production example) and kubeconform all pass, mirroring the release CI
steps. The rendered webapp Deployment and ClickHouse StatefulSet resolve
to the same datastore secret key.
- Inserts using
`input_format_json_infer_array_of_dynamic_from_array_of_different_types`
(the setting that fails on 25.7.5) succeed on the upgraded volume.

## Upgrade preflight and docs

A production upgrade report on this branch surfaced two hazards that
predate this PR — both landed in chart 4.5.6 (#4316) — so they are fixed
here rather than left for the next person to hit.

**`secrets.existingSecret` gained two required keys.** The webapp
started reading `PROVIDER_SECRET` and `COORDINATOR_SECRET`, and when
`existingSecret` is set the chart generates nothing, so a missing key
only surfaced as a `CreateContainerConfigError` partway through the
webapp rollout. The pre-install/pre-upgrade validation now looks the
Secret up and fails with the complete list of missing keys, leaving the
running release untouched. It is skipped under `helm template` and
client-side dry-run, where `lookup` cannot read the cluster.

**Bundled datastore credentials moved into the chart-managed Secret**
(`<release>-clickhouse`/`admin-password` →
`trigger-datastore`/`clickhouse-admin-password`). The chart wires both
ends itself, but consumers outside it — maintenance CronJobs, Grafana
datasources, secret syncs — have to be repointed. A new `## Upgrading`
section in the Kubernetes docs carries the old→new mapping, the two new
keys, and a pointer to the ClickHouse image notes.

The existingSecret key list in the docs also named
`OBJECT_STORE_ACCESS_KEY_ID`/`OBJECT_STORE_SECRET_ACCESS_KEY`, which are
env var names rather than keys the chart reads; corrected to the real
key names and the condition under which they apply.

Verified on a throwaway kind cluster with `--dry-run=server`: a
pre-4.5.6 Secret fails with both key names listed, the documented
`kubectl patch` clears it, and default values, `existingClaim`, external
ClickHouse, volumePermissions/scheduling and the production example all
still render. A real `helm install` followed by an upgrade against an
incomplete Secret aborts with the release still at revision 1 and
`deployed`. `helm lint`, the CI render and kubeconform (59 resources, 0
invalid) pass.

---------

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2026-08-01 14:17:27 +01:00

515 lines
18 KiB
Plaintext

---
title: "Docker compose"
description: "You can self-host Trigger.dev on your own infrastructure using Docker."
---
The following instructions will use docker compose to spin up a Trigger.dev instance. Make sure to read the self-hosting [overview](/self-hosting/overview) first.
As self-hosted deployments tend to have unique requirements and configurations, we don't provide specific advice for securing your deployment, scaling up, or improving reliability.
Should the burden ever get too much, we'd be happy to see you on [Trigger.dev cloud](https://trigger.dev/pricing) where we deal with these concerns for you.
**Warning:** This guide alone is unlikely to result in a production-ready deployment. Security, scaling, and reliability concerns are not fully addressed here.
## What's new?
Goodbye v3, hello v4! We made quite a few changes:
- **Much simpler setup.** The provider and coordinator are now combined into a single supervisor. No more startup scripts, just `docker compose up`.
- **Automatic container cleanup.** The supervisor will automatically clean up containers that are no longer needed.
- **Support for multiple worker machines.** This is a big one, and we're very excited about it! You can now scale your workers horizontally as needed.
- **Resource limits enforced by default.** This means that tasks will be limited to the total CPU and RAM of the machine preset, preventing noisy neighbours.
- **No direct Docker socket access.** The compose file now comes with [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) by default. Yes, you want this.
- **No host networking.** All containers are now running with network isolation, using only the network access they need.
- **No checkpoint support.** This was only ever experimental when self-hosting and not recommended. It caused a bunch of issues. We decided to focus on the core features instead.
- **Built-in container registry and object storage.** You can now deploy and execute tasks without needing third party services for this.
- **Improved CLI commands.** You don't need any additional flags to deploy anymore, and there's a new command to easily `switch` between profiles.
- **Whitelisting for GitHub OAuth.** Any whitelisted email addresses will now also apply to sign ins via GitHub, unlike v3 where they only applied to magic links.
## Requirements
These are the minimum requirements for running the webapp and worker components. They can run on the same, or on separate machines.
It's fine to run everything on the same machine for testing. To be able to scale your workers, you will want to run them separately.
### Prerequisites
To run the webapp and worker components, you will need:
- [Docker](https://docs.docker.com/get-docker/) 20.10.0+
- [Docker Compose](https://docs.docker.com/compose/install/) 2.20.0+
### Webapp
This machine will host the webapp, postgres, redis, and related services.
- 3+ vCPU
- 6+ GB RAM
### Worker
This machine will host the supervisor and all of the runs.
- 4+ vCPU
- 8+ GB RAM
How many workers and resources you need will depend on your workloads and concurrency requirements.
For example:
- 10 concurrency x `small-1x` (0.5 vCPU, 0.5 GB RAM) = 5 vCPU and 5 GB RAM
- 20 concurrency x `small-1x` (0.5 vCPU, 0.5 GB RAM) = 10 vCPU and 10 GB RAM
- 100 concurrency x `small-1x` (0.5 vCPU, 0.5 GB RAM) = 50 vCPU and 50 GB RAM
- 100 concurrency x `small-2x` (1 vCPU, 1 GB RAM) = 100 vCPU and 100 GB RAM
You may need to spin up multiple workers to handle peak concurrency. The good news is you don't have to know the exact numbers upfront. You can start with a single worker and add more as needed.
## Setup
### Webapp
1. Clone the repository
```bash
git clone --depth=1 https://github.com/triggerdotdev/trigger.dev
cd trigger.dev/hosting/docker
```
2. Create a `.env` file and generate secrets
```bash
cp .env.example .env
# Fills the required secrets in .env with strong, unique values.
# Safe to re-run - it never overwrites a secret you've already set.
./generate-secrets.sh
```
<Note>
The stack ships no working default credentials. `generate-secrets.sh` fills the
application secrets and the bundled datastore passwords with strong, unique values.
Keep them safe - rotating the encryption key or session secret later will invalidate
existing sessions and encrypted data.
</Note>
3. Start the webapp
```bash
cd webapp
docker compose up -d
```
4. Configure the webapp using the [environment variables](/self-hosting/env/webapp) in your `.env` file, then apply the changes:
```bash
docker compose up -d
```
5. You should now be able to access the webapp at `http://localhost:8030`. When logging in, check the container logs for the magic link:
```bash
docker compose logs -f webapp
```
6. (optional) To initialize a new project, run the following command:
```bash
npx trigger.dev@latest init -p <project-ref> -a http://localhost:8030
```
### Worker
1. Clone the repository
```bash
git clone --depth=1 https://github.com/triggerdotdev/trigger.dev
cd trigger.dev/hosting/docker
```
2. Create a `.env` file
```bash
cp .env.example .env
```
3. Start the worker
```bash
cd worker
docker compose up -d
```
4. Configure the supervisor using the [environment variables](/self-hosting/env/supervisor) in your `.env` file, including the [worker token](#worker-token).
<Warning>
For a split webapp/worker setup, set `MANAGED_WORKER_SECRET` on the worker to
the **same** value as the webapp's `MANAGED_WORKER_SECRET`. Don't run
`generate-secrets.sh` on the worker host - it would create a mismatched value
and the worker would fail to authenticate.
</Warning>
5. Apply the changes:
```bash
docker compose up -d
```
6. Repeat as needed for additional workers.
### Combined
If you want to run the webapp and worker on the same machine, just replace the `up` command with the following:
```bash
# Run this from the /hosting/docker directory
docker compose -f webapp/docker-compose.yml -f worker/docker-compose.yml up -d
```
## Worker token
When running the combined stack, worker bootstrap is handled automatically. When running the webapp and worker separately, you will need to manually set the worker token.
On the first run, the webapp will generate a worker token and store it in a shared volume. It will also print the token to the console. It should look something like this:
```bash
==========================
Trigger.dev Bootstrap - Worker Token
WARNING: This will only be shown once. Save it now!
Worker group:
bootstrap
Token:
tr_wgt_fgfAEjsTmvl4lowBLTbP7Xo563UlnVa206mr9uW6
If using docker compose, set:
TRIGGER_WORKER_TOKEN=tr_wgt_fgfAEjsTmvl4lowBLTbP7Xo563UlnVa206mr9uW6
Or, if using a file:
TRIGGER_WORKER_TOKEN=file:///home/node/shared/worker_token
==========================
```
You can then uncomment and set the `TRIGGER_WORKER_TOKEN` environment variable in your `.env` file.
Don't forget to restart the worker container for the changes to take effect:
```bash
# Run this from the /hosting/docker/worker directory
docker compose down
docker compose up -d
```
### Creating additional worker groups
To create additional worker groups beyond the bootstrap group, use the admin API endpoint. This requires admin privileges.
**Making a user admin:**
- **New users**: Set `ADMIN_EMAILS` environment variable (regex pattern) before user creation.
- **Existing users**: Set `admin = true` in the `user` table in your database.
**Creating a worker group:**
```bash
api_url=http://localhost:8030
wg_name=my-worker
admin_pat=tr_pat_...
curl -X POST \
"$api_url/admin/api/v1/workers" \
-H "Authorization: Bearer $admin_pat" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$wg_name\"}"
```
The response includes a `token` field if the worker group is newly created.
## Registry setup
The registry is used to store and pull deployment images. When testing the stack locally, the defaults should work out of the box.
When deploying to production, you will need to set the correct URL and generate secure credentials for the registry.
### Default settings
The default settings for the registry are:
- Registry: `localhost:5000`
- Username: `registry-user`
- Password: `very-secure-indeed`
You should change these before deploying to production, especially the password. You can find more information about how to do this in the official [registry docs](https://github.com/distribution/distribution/blob/735c161b53e7faf81a21ba94c55ac9edee081cd9/docs/deploying.md#native-basic-auth).
**Note:** This will require modifying the default `.htpasswd` file located at `./hosting/docker/registry/auth.htpasswd` of the repo root.
### Logging in
When self-hosting, builds run locally. You will have to login to the registry on every machine that runs the `deploy` command. You should only have to do this once:
```bash
docker login -u <username> <registry>
```
This will prompt for the password. Afterwards, the deploy command should work as expected.
## Object storage
This is mainly used for large payloads and outputs. There are a few simple steps to follow to get started.
### Default settings
The default settings for the object storage are:
- Endpoint: `http://localhost:9000`
- Username: `admin`
- Password: `very-safe-password`
You should change these before deploying to production, especially the password.
### Setup
<Note>
The `packets` bucket is created by default. In case this doesn't work, you can create it manually.
</Note>
1. Login to the dashboard: `http://localhost:9001`
2. Create a bucket named `packets`.
3. For production, you will want to set up a dedicated user and not use the root credentials above.
## Authentication
The specific set of variables required will depend on your choice of email transport or alternative login methods like GitHub OAuth.
### Magic link
By default, magic link auth is the only login option. If the `EMAIL_TRANSPORT` env var is not set, the magic links will be logged by the webapp container and not sent via email.
#### Resend
```bash
EMAIL_TRANSPORT=resend
FROM_EMAIL=
REPLY_TO_EMAIL=
RESEND_API_KEY=<your_resend_api_key>
```
#### SMTP
Note that setting `SMTP_SECURE=false` does _not_ mean the email is sent insecurely.
This simply means that the connection is secured using the modern STARTTLS protocol command instead of implicit TLS.
You should only set this to true when the SMTP server host directs you to do so (generally when using port 465)
```bash
EMAIL_TRANSPORT=smtp
FROM_EMAIL=
REPLY_TO_EMAIL=
SMTP_HOST=<your_smtp_server>
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=<your_smtp_username>
SMTP_PASSWORD=<your_smtp_password>
```
#### AWS SES
Credentials are to be supplied as with any other program using the AWS SDK.
In this scenario, you would likely either supply the additional environment variables `AWS_REGION`, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` or, when running on AWS, use credentials supplied by the EC2 IMDS.
```bash
EMAIL_TRANSPORT=aws-ses
FROM_EMAIL=
REPLY_TO_EMAIL=
```
### GitHub OAuth
To authenticate with GitHub, you will need to set up a GitHub OAuth app. It needs a callback URL `https://<your_webapp_domain>/auth/github/callback` and you will have to set the following env vars:
```bash
AUTH_GITHUB_CLIENT_ID=<your_client_id>
AUTH_GITHUB_CLIENT_SECRET=<your_client_secret>
```
### Restricting access
All email addresses can sign up and log in this way. If you would like to restrict this, you can use the `WHITELISTED_EMAILS` env var. For example:
```bash
# every email that does not match this regex will be rejected
WHITELISTED_EMAILS="^(authorized@yahoo\.com|authorized@gmail\.com)$"
```
This will apply to all auth methods including magic link and GitHub OAuth.
## Version locking
There are several reasons to lock the version of your Docker images:
- **Backwards compatibility.** We try our best to maintain compatibility with older CLI versions, but it's not always possible. If you don't want to update your CLI, you can lock your Docker images to that specific version.
- **Ensuring full feature support.** Sometimes, new CLI releases will also require new or updated platform features. Running unlocked images can make any issues difficult to debug. Using a specific tag can help here as well.
By default, the images will point at the latest versioned release via the `latest` tag. You can override this by specifying a different tag in your `.env` file. For example:
```bash
TRIGGER_IMAGE_TAG=v4.5.0
```
We patch the latest released version line only, so keep an eye on new releases to receive security fixes. See [Security & vulnerability reporting](/self-hosting/security).
You can also lock the versions of the bundled services, for example with `CLICKHOUSE_IMAGE_TAG`. If you do, or if you bring your own ClickHouse via `CLICKHOUSE_URL`, note that Trigger.dev requires ClickHouse 25.8 or newer.
<Note>
The bundled ClickHouse now uses the official `clickhouse/clickhouse-server` image. Your existing
data volume carries over automatically. If you previously pinned `CLICKHOUSE_IMAGE_TAG` to a
Bitnami tag (for example `25.7.5-debian-12-r0`), update it to an official image tag such as
`26.2` — Bitnami tags don't exist in the official repository. Note the switch is one-way: the
official image takes ownership of the data files, so rolling back to the Bitnami image requires
manually restoring their previous owner (`chown -R 1001:1001` on the volume).
</Note>
<Note>
Trigger.dev 4.5.0 is the last version we officially support for running v3 (SDK v3) tasks. If
you still have v3 tasks, pin `TRIGGER_IMAGE_TAG` to exactly `v4.5.0` or [migrate to
v4](/migrating-from-v3). 4.5.1 and later reject v3 triggers and deploys with an upgrade message.
</Note>
## Task events
By default, task events (timeline, logs, spans) are stored in PostgreSQL. For production deployments we recommend storing them in ClickHouse instead, it scales to much higher volumes and avoids unbounded growth of the `TaskEvent` table.
To enable, set on the webapp in your `.env`:
```bash
EVENT_REPOSITORY_DEFAULT_STORE=clickhouse_v2
```
This only affects new runs; existing runs continue to read from wherever their events were originally stored.
## Realtime streams
Realtime streams power AI-agent token streaming and run streams. They default to **v2**, backed by the bundled `s2` service — [s2-lite](https://s2.dev), the open-source, self-hostable S2 server. It stores stream data in a persistent volume and is preconfigured in the webapp compose file, so no setup is required.
To fall back to the Redis-backed **v1** streams, set on the webapp in your `.env`:
```bash
REALTIME_STREAMS_DEFAULT_VERSION=v1
```
To use a hosted S2 at [s2.dev](https://s2.dev) instead of the bundled s2-lite, point the endpoint at your basin and supply an access token:
```bash
REALTIME_STREAMS_S2_BASIN=your-basin
REALTIME_STREAMS_S2_ENDPOINT=https://your-basin.b.aws.s2.dev/v1
REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS=false
REALTIME_STREAMS_S2_ACCESS_TOKEN=your-access-token
```
See the [webapp environment variables](/self-hosting/env/webapp) for the full list of realtime stream settings.
## Troubleshooting
- **Deployment fails at the push step.** The machine running `deploy` needs registry access. See the [registry setup](#registry-setup) section for more details.
- **Magic links don't arrive.** The webapp container needs to be able to send emails. You probably need to set up an email transport. See the [authentication](#authentication) section for more details.
You should check the logs of the webapp container to see the magic link:
```bash
# Run this from the /hosting/docker/webapp directory
docker compose logs -f webapp
```
- **ClickHouse migrations say "no migrations to run" but schema is missing.** The goose migration tracker is out of sync. Exec into the webapp container, set the GOOSE env vars (from webapp startup logs), and run `goose reset && goose up`.
<Warning>
**Data Loss Warning:** The `goose reset` command is destructive and will drop the entire schema.
Make sure to backup your data and confirm you are running this in a non-production environment
before executing this command.
</Warning>
## CLI usage
This section highlights some of the CLI commands and options that are useful when self-hosting. Please check the [CLI reference](/cli-introduction) for more in-depth documentation.
### Login
To avoid being redirected to [Trigger.dev Cloud](https://cloud.trigger.dev) when using the CLI, you need to specify the URL of your self-hosted instance with the `--api-url` or `-a` flag. For example:
```bash
npx trigger.dev@latest login -a http://trigger.example.com
```
Once you've logged in, you shouldn't have to specify the URL again with other commands.
### Profiles
You can specify a profile when logging in. This allows you to easily use the CLI with multiple instances of Trigger.dev. For example:
```bash
npx trigger.dev@latest login -a http://trigger.example.com \
--profile self-hosted
```
Logging in with a new profile will also make it the new default profile.
To use a specific profile, you can use the `--profile` flag with other commands:
```bash
npx trigger.dev@latest dev --profile self-hosted
```
To list all your profiles, use the `list-profiles` command:
```bash
npx trigger.dev@latest list-profiles
```
To remove a profile, use the `logout` command:
```bash
npx trigger.dev@latest logout --profile self-hosted
```
To switch to a different profile, use the `switch` command:
```bash
# To run interactively
npx trigger.dev@latest switch
# To switch to a specific profile
npx trigger.dev@latest switch self-hosted
```
### Whoami
It can be useful to check you are logged into the correct instance. Running this will also show the API URL:
```bash
npx trigger.dev@latest whoami
```
## CI / GitHub Actions
When running the CLI in a CI environment, your login profiles won't be available. Instead, you can use the `TRIGGER_API_URL` and `TRIGGER_ACCESS_TOKEN` environment
variables to point at your self-hosted instance and authenticate.
For more detailed instructions, see the [GitHub Actions guide](/github-actions).
## Telemetry
By default, the Trigger.dev webapp sends telemetry data to our servers. This data is used to improve the product and is not shared with third parties. If you would like to opt-out of this, you can set the `TRIGGER_TELEMETRY_DISABLED` environment variable on the webapp container. The value doesn't matter, it just can't be empty. For example:
```yaml
services:
webapp:
...
environment:
TRIGGER_TELEMETRY_DISABLED: 1
```