This guide provides production-ready Docker Compose configurations for deploying Vaultwarden on Linux servers. Includes multiple deployment scenarios, security hardening, and operational best practices.
Current Stable Version:
1.35.4(February 2026) | Image:ghcr.io/dani-garcia/vaultwarden:1.35.4
For testing or personal use:
# docker-compose.yml
services:
vaultwarden:
image: ghcr.io/dani-garcia/vaultwarden:1.35.4
container_name: vaultwarden
restart: unless-stopped
volumes:
- ./vw-data:/data
ports:
- "127.0.0.1:8000:80"
environment:
- DOMAIN=https://vault.example.com
- SIGNUPS_ALLOWED=false
- ADMIN_TOKEN=change-this-to-secure-token
# Start the service
docker compose up -d
# Verify
docker compose ps
docker logs vaultwarden
Production-ready setup with security hardening, WebSocket support, and PostgreSQL database:
# docker-compose.yml
version: '3.8'
services:
# =============================================================================
# Vaultwarden Password Server
# =============================================================================
vaultwarden:
image: ghcr.io/dani-garcia/vaultwarden:1.35.4
container_name: vaultwarden
restart: unless-stopped
user: "1000:1000"
# Security hardening
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
# Volumes
volumes:
- ./vw-data:/data:Z
- ./vw-data/attachments:/data/attachments:Z
- ./vw-data/icons:/data/icons:Z
- ./vw-data/sends:/data/sends:Z
# Environment variables
environment:
# Core settings
- DOMAIN=https://vault.example.com
- DATABASE_URL=postgresql://vaultwarden:secure_password@postgres:5432/vaultwarden
# Security
- SIGNUPS_ALLOWED=false
- SIGNUPS_VERIFY=false
- ADMIN_TOKEN=${ADMIN_TOKEN}
- LOGIN_RATELIMIT_MAX_BURST=5
- LOGIN_RATELIMIT_SECONDS=120
- IP_HEADER=X-Forwarded-For
# WebSocket
- WEBSOCKET_ENABLED=true
- WEBSOCKET_PORT=3012
# Features
- ORGANIZATIONS_ALLOWED=true
- ATTACHMENTS_ALLOWED=true
- SEND_ALLOWED=true
- EMERGENCY_ACCESS_ALLOWED=true
- EVENTS_ENABLED=true
- EVENTS_DAYS_RETAIN=90
# Performance
- NUM_WORKERS=4
# Logging
- LOG_LEVEL=info
- LOG_FILE=/data/vaultwarden.log
- LOG_TO_FILE=true
# Network
networks:
- vaultwarden_internal
depends_on:
postgres:
condition: service_healthy
# =============================================================================
# PostgreSQL Database
# =============================================================================
postgres:
image: postgres:16-alpine
container_name: vaultwarden_postgres
restart: unless-stopped
user: "999:999"
volumes:
- ./postgres-data:/var/lib/postgresql/data:Z
- ./postgres-init:/docker-entrypoint-initdb.d:Z
environment:
- POSTGRES_USER=vaultwarden
- POSTGRES_PASSWORD=secure_password
- POSTGRES_DB=vaultwarden
- PGDATA=/var/lib/postgresql/data/pgdata
# Health check
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vaultwarden -d vaultwarden"]
interval: 10s
timeout: 5s
retries: 5
networks:
- vaultwarden_internal
# Resource limits
deploy:
resources:
limits:
memory: 512M
# =============================================================================
# Nginx Reverse Proxy
# =============================================================================
nginx:
image: nginx:alpine
container_name: vaultwarden_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./nginx/logs:/var/log/nginx
depends_on:
- vaultwarden
networks:
- vaultwarden_internal
- vaultwarden_external
# =============================================================================
# Networks
# =============================================================================
networks:
vaultwarden_internal:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
vaultwarden_external:
driver: bridge
# =============================================================================
# Volumes (named volumes alternative)
# =============================================================================
volumes:
vw-data:
driver: local
postgres-data:
driver: local
Create a .env file for sensitive values:
# .env
# =============================================================================
# VAULTWARDEN DOCKER ENVIRONMENT
# =============================================================================
# Admin token (generate with: openssl rand -base64 48)
ADMIN_TOKEN=your-32-character-secure-token-here
# Database password (generate with: openssl rand -base64 32)
POSTGRES_PASSWORD=your-secure-database-password
# Domain configuration
DOMAIN=https://vault.example.com
# Optional: Email domains whitelist (comma-separated)
# SIGNUPS_DOMAINS_WHITELIST=example.com,corp.example.com
Create nginx/conf.d/vaultwarden.conf:
# =============================================================================
# Vaultwarden Reverse Proxy Configuration
# =============================================================================
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=vaultwarden_limit:10m rate=10r/s;
# HTTP → HTTPS redirect
server {
listen 80;
server_name vault.example.com;
# ACME challenge for Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
server_name vault.example.com;
# SSL configuration (TLS 1.3)
ssl_protocols TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Certificate paths
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss://$host; frame-ancestors 'none';" always;
# Access logging
access_log /var/log/nginx/vaultwarden_access.log;
error_log /var/log/nginx/vaultwarden_error.log;
# Main application
location / {
limit_req zone=vaultwarden_limit burst=20 nodelay;
proxy_pass http://vaultwarden:80;
proxy_http_version 1.1;
# Headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering off;
proxy_cache off;
}
# WebSocket notifications endpoint
location /notifications/hub {
proxy_pass http://vaultwarden:3012;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Admin panel - restrict access
location /admin {
# Allow only internal networks
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 172.16.0.0/12;
deny all;
proxy_pass http://vaultwarden:80;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
services:
vaultwarden:
image: ghcr.io/dani-garcia/vaultwarden:1.35.3
volumes:
- ./vw-data:/data
environment:
- DATABASE_URL=/data/db.sqlite3
services:
vaultwarden:
environment:
- DATABASE_URL=postgresql://user:password@postgres:5432/vaultwarden
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=vaultwarden
- POSTGRES_PASSWORD=secure_password
- POSTGRES_DB=vaultwarden
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vaultwarden"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
services:
vaultwarden:
environment:
- DATABASE_URL=mysql://user:password@mariadb:3306/vaultwarden
mariadb:
image: mariadb:11
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=vaultwarden
- MYSQL_USER=vaultwarden
- MYSQL_PASSWORD=secure_password
volumes:
- mariadb-data:/var/lib/mysql
| Image Tag | Base | Size | Use Case |
|---|---|---|---|
latest |
Debian | ~100MB | General purpose |
1.35.3 |
Debian | ~100MB | Pinned stable |
alpine |
Alpine | ~50MB | Minimal footprint |
mysql |
Debian + MySQL | ~150MB | MySQL support |
Recommended: Use pinned version tags for production (1.35.3), not latest.
#!/bin/bash
# backup-vaultwarden.sh
set -e
BACKUP_DIR="/backup/vaultwarden"
DATE=$(date +%Y%m%d_%H%M%S)
DATA_DIR="/opt/vaultwarden/vw-data"
mkdir -p "${BACKUP_DIR}"
# Stop container for consistent backup (optional)
# docker compose stop vaultwarden
# Create backup
tar -czf "${BACKUP_DIR}/vaultwarden_${DATE}.tar.gz" \
-C "$(dirname "${DATA_DIR}")" \
"$(basename "${DATA_DIR}")"
# Restart if stopped
# docker compose start vaultwarden
# Retain only last 30 days
find "${BACKUP_DIR}" -name "vaultwarden_*.tar.gz" -mtime +30 -delete
echo "Backup completed: ${BACKUP_DIR}/vaultwarden_${DATE}.tar.gz"
# 1. Check current version
docker compose ps
# 2. Pull latest image
docker compose pull vaultwarden
# 3. Backup data
./backup-vaultwarden.sh
# 4. Recreate container
docker compose up -d --force-recreate vaultwarden
# 5. Verify
docker logs vaultwarden | grep "Starting Vaultwarden"
docker compose ps
# View logs
docker compose logs -f vaultwarden
# Check resource usage
docker stats vaultwarden
# Execute commands in container
docker compose exec vaultwarden sh
# Database export (SQLite)
docker compose exec vaultwarden sqlite3 /data/db.sqlite3 ".dump" > backup.sql
# Health check
docker compose exec vaultwarden curl -s http://localhost:80/alive
| Issue | Solution |
|---|---|
| Container won’t start | Check logs: docker compose logs vaultwarden |
| Database connection failed | Verify DB container is healthy |
| WebSocket not working | Check Nginx WebSocket configuration |
| Permission denied | Ensure correct user/volume permissions |
| High memory usage | Adjust NUM_WORKERS and container limits |
# Enable extended logging
environment:
- EXTENDED_LOGGING=true
- LOG_LEVEL=debug
Any questions?
Feel free to contact us. Find all contact information on our contact page.