Docker Image and Compose
Run Durabull with the official Docker image, then scale to a production-friendly Compose deployment.
Docker is the fastest way to get a self-hosted Durabull instance running.
1) Choose an Image Tag
Use the published image from GHCR:
ghcr.io/durabullhq/durabull:<version>
For local evaluation, latest is fine. For production, pin an explicit version tag.
2) Quick Run with docker run
This starts Redis and Durabull on a shared Docker network:
docker network create durabull
docker run -d --name durabull-redis --network durabull redis:8-alpine
docker run --rm -p 3000:3000 --network durabull \
-e DURABULL_AUTHLESS=true \
-e DURABULL_ENV_CONNECTIONS=true \
-e DURABULL_REDIS_URL_ENCRYPTION_KEY=$(openssl rand -hex 32) \
-e DURABULL_REDIS_URL_MAIN=redis://durabull-redis:6379 \
-e DURABULL_REDIS_URL_MAIN_ENVIRONMENT=development \
-e DURABULL_REDIS_URL_DEFAULT=MAIN \
-e APP_BASE_URL=http://localhost:3000 \
-e VITE_PUBLIC_APP_URL=http://localhost:3000 \
ghcr.io/durabullhq/durabull:latest
Open http://localhost:3000, then confirm:
GET /api/healthreturnsstatus: okGET /api/app/configreturns JSON instead of500- the queue dashboard loads for your default connection
3) Docker Compose (Durabull + Redis)
Use Compose for repeatable team setup:
name: durabull-self-hosted
services:
durabull:
image: ghcr.io/durabullhq/durabull:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
DURABULL_AUTHLESS: "true"
DURABULL_ENV_CONNECTIONS: "true"
DURABULL_REDIS_URL_ENCRYPTION_KEY: "${DURABULL_REDIS_URL_ENCRYPTION_KEY}"
DURABULL_REDIS_URL_MAIN: redis://redis:6379
DURABULL_REDIS_URL_MAIN_ENVIRONMENT: development
DURABULL_REDIS_URL_DEFAULT: MAIN
APP_BASE_URL: http://localhost:3000
VITE_PUBLIC_APP_URL: http://localhost:3000
volumes:
- durabull_data:/app/data
depends_on:
redis:
condition: service_healthy
redis:
image: redis:8-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
command: redis-server --appendonly yes
volumes:
durabull_data:
redis_data:
Start it with:
export DURABULL_REDIS_URL_ENCRYPTION_KEY="$(openssl rand -hex 32)"
docker compose -f tooling/docker/docker-compose.self-hosted.yaml up -d
Then validate:
GET /api/healthGET /api/app/configGET /api/auth/get-sessionwhenDURABULL_AUTHLESS=true
An example file is included at tooling/docker/docker-compose.self-hosted.yaml.
4) Persistence Notes
When DATABASE_URL is unset, PGlite data is file-backed inside the container.
- Default location:
/app/data/pglite - For persistence, mount
/app/dataor setDURABULL_PGLITE_DIRto a mounted path. - Named Docker volumes are the simplest option for local deployments.
- If you bind-mount a host directory, ensure it is writable by container UID/GID
1000:1000(bun).
Older Image Workaround
If you are pinned to an older image that fails with EACCES: permission denied, mkdir '/app/data' or returns 500 for /api/app/config, use one of these temporary workarounds:
- upgrade to an image that includes the writable
/app/datafix - set
DURABULL_PGLITE_DIRto a writable path inside the container for ephemeral evaluation - switch to PostgreSQL mode with
DATABASE_URL
5) Production Notes
For internet-facing deployments:
- set
DURABULL_AUTHLESS=false - configure
BETTER_AUTH_SECRET - configure
DURABULL_REDIS_URL_ENCRYPTION_KEY - set correct
APP_BASE_URLandVITE_PUBLIC_APP_URL - review Security and Hardening
- use PostgreSQL (
DATABASE_URL) for stateful team environments
6) Build Image from Source
Source Dockerfile path:
tooling/docker/Dockerfiletooling/docker/docker-compose.self-hosted.yaml
It uses turbo prune and multi-stage builds for optimized production images.