Why self-host Trigger.dev on a VPS
Trigger.dev replaces homemade queues (BullMQ, fragile crons, Lambdas that time out) with a unified platform: durable tasks, automatic retries, concurrency management and a real-time dashboard. The cloud version bills by the number of executions and limits job duration, which quickly becomes constraining for ETL processing, mass email sending or slow API calls.
Self-hosted on a VPS, you keep your workers on your side: your third-party API keys (OpenAI, Stripe, Resend) never pass through a third party, and your jobs can run for 10 minutes or several hours without being cut off. The stack relies on PostgreSQL, Redis and Docker, making it reproducible and easy to back up. The installation is completely free: no licence, no subscription — only the VPS cost applies.
What you gain by self-hosting
- No limit on the duration or number of executions of your background tasks.
- Your secrets (API keys, tokens) stay in your infrastructure, never on a third-party SaaS.
- Predictable fixed cost: a single VPS instead of a bill that climbs with volume.
- No public network hop if your workers run next to your database and your application.
- Full control over versions, environment variables and log retention.
- The ability to trigger jobs from your internal webhooks without exposing any external service.
- Update at your own pace: you choose when to upgrade to a new version of Trigger.dev.
Trigger.dev self-hosted vs cloud: what actually changes
Scroll the table
| Criterion | Cloud (SaaS) | Self-hosted VPS |
|---|---|---|
| Maximum job duration | Limited (a few minutes) | No duration limit |
| Cost at volume | Grows with executions | Fixed (monthly VPS) |
| Secrets / API keys | Pass through the cloud | Stay in your infra |
| Updates | Automatic, imposed | At your own pace |
| Internal network access | Impossible without tunnel | Direct, no round-trip via the public Internet |
| Log retention | Limited by plan | Controlled by you |
| Initial setup | Zero configuration | ~30 minutes (this guide) |
Hardware and software requirements
Trigger.dev is a relatively resource-hungry stack because it combines the web application, workers, PostgreSQL and Redis. Plan for a VPS with at least 4 GB of RAM and 2 vCPU for light production use; aim for 8 GB of RAM and 4 vCPU if you run many concurrent jobs or heavy processing. Allow 40 GB of SSD disk for the database, logs and Docker images.
On the software side: Docker Engine 24+ and the Docker Compose v2 plugin (check with docker compose version), a domain name pointing to the VPS IP (for example trigger.mydomain.com), and port 443 open for HTTPS. Two secrets are essential: MAGIC_LINK_SECRET (passwordless authentication) and ENCRYPTION_KEY (32 hexadecimal characters, encrypts project environment variables). Generate them before you begin — they cannot be regenerated without breaking existing data.
Install Trigger.dev with Docker in 6 steps
Prepare the VPS and install Docker
Connect over SSH with a sudo user, update the system (
apt update && apt upgrade -y) then install Docker in one command:curl -fsSL https://get.docker.com | sh. Add your user to the docker group (usermod -aG docker $USER) to avoid always usingsudo. Create a dedicated folder:mkdir -p /opt/trigger && cd /opt/trigger. Verify that Compose v2 is available withdocker compose version— the output should show at leastv2.x.x.Fetch the official self-hosting stack
Clone the official repository:
git clone https://github.com/triggerdotdev/trigger.dev /opt/trigger/src. Navigate to the deployment subfolder:cd /opt/trigger/src/docker. This directory contains thedocker-compose.ymlthat orchestrates the web application (webapp), workers, PostgreSQL and Redis. Copy the example file to your configuration:cp .env.example .env. Do not launch anything before configuring the.env— the stack will refuse to start with default values.Generate the secrets and configure the domain
Open
.envin your editor and set at minimum these variables:-
ENCRYPTION_KEY:openssl rand -hex 16(16 bytes = 32 hex chars)
-MAGIC_LINK_SECRET:openssl rand -hex 16(same)
-LOGIN_ORIGIN:https://trigger.mydomain.com
-APP_ORIGIN:https://trigger.mydomain.com
-POSTGRES_PASSWORD: a strong password (openssl rand -base64 24)
-REDIS_PASSWORD: sameLeave
DATABASE_URLandREDIS_URLas-is if you use the internal Compose services — they reference Docker service names. Also setSESSION_SECRETwithopenssl rand -hex 32.Launch the stack and create the first account
Start everything in the background:
docker compose up -d. Follow the database migrations in real time:docker compose logs -f webapp. Wait for the messageListening on port 3030before continuing — migrations can take 30 to 60 seconds on first startup. Once the application has started, the signup magic link appears in the logs: copy it and open it in your browser to create the first admin account. If you missed the link:docker compose logs webapp | grep magic.Expose the application via an HTTPS reverse proxy
Place Caddy in front of the application to handle TLS automatically. Create
/etc/caddy/Caddyfilewith this minimal content:trigger.mydomain.com { reverse_proxy localhost:3030 }Reload Caddy (
systemctl reload caddy): Let's Encrypt issues the certificate in seconds. With nginx, create a vhost that proxies tohttp://127.0.0.1:3030and activate a certificate viacertbot --nginx. Then verify thathttps://trigger.mydomain.comresponds correctly before moving on.Connect your first TypeScript project
In your Node.js project, install the SDK:
npm install @trigger.dev/sdk. Authenticate the CLI against your instance:npx trigger.dev@latest login --api-url https://trigger.mydomain.com. Then create your first project in the dashboard, retrieve the project secret key (sk_...) and add it to your local.env:TRIGGER_SECRET_KEY=sk_.... Initialise the configuration withnpx trigger.dev@latest initand deploy your first job withnpx trigger.dev@latest deploy.
Troubleshooting: common errors and their solutions
Here are the five most common errors encountered when installing Trigger.dev self-hosted.
Error: ENCRYPTION_KEY must be 32 characters — You used openssl rand -base64 16 instead of openssl rand -hex 16. The base64 form produces characters outside the hexadecimal character set. Regenerate with -hex 16 (exactly 32 chars).
webapp exited with code 1 on startup — Check the full logs with docker compose logs webapp. Most common cause: incorrect DATABASE_URL or PostgreSQL not yet ready. Wait 10 seconds and restart with docker compose restart webapp.
The account creation magic link does not appear — The application may have started before migrations finished. Restart docker compose restart webapp and watch the logs from the beginning. If the link is still absent, check that LOGIN_ORIGIN exactly matches your domain (no trailing slash).
Failed to connect in the CLI during login — The reverse proxy is not yet active or DNS has not yet propagated to your VPS. Test locally with curl http://127.0.0.1:3030/healthcheck from the VPS: if it responds, the issue is on the proxy or DNS side.
The job deploys but does not run — Check that the workers are running: docker compose ps should show the worker service in running state. If the worker is stopped, docker compose up -d worker restarts it. Also check that TRIGGER_SECRET_KEY in your project matches the key of the correct project in the dashboard.
Securing and maintaining your instance
A Trigger.dev instance exposes the dashboard and the API on the same domain. A few precautions reduce the attack surface without complicating operations.
Firewall: block all ports except 22 (SSH), 80 and 443 (with ufw allow for each). Port 3030 must never be directly accessible from outside — it is reserved for the local reverse proxy.
Secret rotation: MAGIC_LINK_SECRET can be rotated without breaking data. ENCRYPTION_KEY, however, encrypts project environment variables — rotation requires a data migration. Store them in a secrets manager (Bitwarden, 1Password or HashiCorp Vault).
Updates: Trigger.dev publishes releases on GitHub. To update, pull the new version (git pull in /opt/trigger/src), rebuild the images (docker compose pull) and restart (docker compose up -d). Database migrations are applied automatically at webapp startup.
Backups: schedule a daily PostgreSQL dump to external storage. A minimal cron: 0 3 * * * docker exec trigger-postgres-1 pg_dump -U postgres trigger | gzip > /opt/backups/trigger-$(date +%F).sql.gz.
Horizontal worker scalability
Isolate the workers from the web application on separate containers and limit their concurrency via the WORKER_CONCURRENCY variable (default: 10). For very CPU-hungry jobs, add a second VPS dedicated to workers pointing to the same PostgreSQL and Redis: you scale horizontally without touching the dashboard. Workers are stateless — they only need DATABASE_URL, REDIS_URL and ENCRYPTION_KEY to join the fleet. On a small VPS, start with WORKER_CONCURRENCY=3 to avoid memory saturation during job spikes.
Writing and deploying your first Trigger.dev job
A Trigger.dev job is a TypeScript function exported from a trigger/ file in your project. Here is a minimal example of a deferred email send:
import { task } from "@trigger.dev/sdk/v3";
export const sendWelcomeEmail = task({
id: "send-welcome-email",
run: async (payload: { userId: string; email: string }) => {
// your sending logic here
await sendEmail(payload.email, "Welcome!");
return { sent: true };
},
});Deploy with npx trigger.dev@latest deploy. The dashboard then shows your job under the Tasks tab. Trigger a test run from the dashboard or from your code: await sendWelcomeEmail.trigger({ userId: "u1", email: "[email protected]" }). Automatic retries apply on error — configurable with the retry option on task().
For long jobs (CSV import, image processing), use wait.for() to suspend execution and avoid blocking a worker during network pauses: Trigger.dev resumes the job where it left off, even after a container restart.
Use cases best suited to self-hosting
- ETL processing: import, transform and load large data volumes without timeouts.
- Mass transactional sends: email or notification campaigns with controlled throttling.
- AI pipelines: chained calls to OpenAI, Anthropic or Hugging Face with retry handling on rate-limits.
- Third-party integration sync: Stripe webhooks, Shopify, HubSpot — without depending on an intermediary SaaS.
- Critical scheduled tasks: replace fragile crons with jobs that have an execution history and alerts.
- PDF report generation or heavy exports: multi-minute jobs with no duration limit.
Trigger.dev is completely free when self-hosted: no commercial licence is required for private or professional use on your own infrastructure. The MIT licence covers the open-source code. Only the proprietary code of the Enterprise version (SAML SSO, advanced audit logs) is excluded — for the vast majority of teams, the self-hosted community version is more than sufficient.