Docker Compose
Run Bulwark alongside Stalwart Mail Server using Docker Compose for a complete, self-contained email stack.
First-launch note (1.6.4+) If you omit
JMAP_SERVER_URLfrom the environment, the web setup wizard runs on first launch and configures the JMAP endpoint, OAuth, branding, and admin password through the browser - no need to author.env.localfirst.
Basic Setup
Create a docker-compose.yml:
services:
stalwart:
image: stalwartlabs/mail-server:latest
container_name: stalwart
ports:
- "443:443"
- "25:25"
- "587:587"
- "993:993"
- "8080:8080"
volumes:
- stalwart-data:/opt/stalwart
restart: unless-stopped
bulwark:
image: ghcr.io/bulwarkmail/webmail:latest
container_name: bulwark
ports:
- "3000:3000"
environment:
HOSTNAME: "0.0.0.0" # Use "::" for IPv6
PORT: "3000"
JMAP_SERVER_URL: http://stalwart:8080
depends_on:
- stalwart
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/api/health",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
volumes:
stalwart-data:
Using env_file
For more complex configurations (OAuth, session secret, branding, etc.), use an environment file:
services:
bulwark:
image: ghcr.io/bulwarkmail/webmail:latest
container_name: bulwark
ports:
- "3000:3000"
env_file:
- .env.local
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/api/health",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
Persistent Volumes
services:
bulwark:
image: ghcr.io/bulwarkmail/webmail:latest
environment:
# JMAP_SERVER_URL set here skips the setup wizard
JMAP_SERVER_URL: http://stalwart:8080
SESSION_SECRET: your-secret-key-here
SETTINGS_SYNC_ENABLED: "true"
volumes:
- bulwark-settings:/app/data/settings # encrypted user settings
- bulwark-config:/app/data/admin # wizard / admin-managed config (1.6.4+)
- bulwark-state:/app/data/admin-state # audit log, login timestamps (1.6.4+)
- bulwark-telemetry:/app/data/telemetry # instance_id + consent
# ...
volumes:
bulwark-settings:
bulwark-config:
bulwark-state:
bulwark-telemetry:
SETTINGS_DATA_DIRdefaults to./data/settings→/app/data/settingsin the container.ADMIN_CONFIG_DIRdefaults to./data/admin→/app/data/admin. After the setup wizard runs you may remount this:roand setADMIN_CONFIG_READONLY=true.ADMIN_STATE_DIRdefaults to./data/admin-state→/app/data/admin-state. Always read-write.- Legacy single-volume installs (
ADMIN_DATA_DIR) are still honoured when neither split variable is set.
Start the Stack
docker compose up -d
View Logs
docker compose logs -f bulwark
docker compose logs -f stalwart
Updating
docker compose pull
docker compose up -d
Custom Build
If you want to build Bulwark from source instead of using the prebuilt image:
bulwark:
build:
context: ./webmail
dockerfile: Dockerfile
container_name: bulwark
ports:
- "3000:3000"
environment:
JMAP_SERVER_URL: http://stalwart:8080
depends_on:
- stalwart
restart: unless-stopped