This guide provides a complete Ansible playbook for deploying Vaultwarden with Docker Compose on Debian, Ubuntu, and RHEL-compatible Linux distributions. Includes security hardening, backup automation, and production-ready configurations.
Current Stable Version:
1.35.4(February 2026)
| Component | Minimum | Recommended |
|---|---|---|
| OS | Debian 10+, Ubuntu 20.04+, RHEL 9+ | Debian 12, Ubuntu 24.04, RHEL 9.3+ |
| CPU | 1 core | 2+ cores |
| RAM | 512 MB | 1+ GB |
| Disk | 5 GB | 20+ GB |
vaultwarden-ansible/
βββ inventory/
β βββ hosts.yml
β βββ group_vars/
β βββ vaultwarden.yml
βββ playbooks/
β βββ deploy.yml
β βββ backup.yml
β βββ update.yml
βββ roles/
β βββ docker/
β βββ vaultwarden/
β βββ nginx/
βββ templates/
β βββ docker-compose.yml.j2
β βββ nginx.conf.j2
β βββ .env.j2
βββ vars/
β βββ main.yml
βββ ansible.cfg
βββ README.md
inventory/hosts.ymlall:
children:
vaultwarden:
hosts:
vault01:
ansible_host: 192.168.1.100
ansible_user: admin
vaultwarden_domain: vault.example.com
vault02:
ansible_host: 192.168.1.101
ansible_user: admin
vaultwarden_domain: vault.internal.example.com
inventory/group_vars/vaultwarden.yml---
# =============================================================================
# Vaultwarden Group Variables
# =============================================================================
# Application settings
vaultwarden_version: "1.35.4"
vaultwarden_app_root: /opt/vaultwarden
vaultwarden_data_dir: /opt/vaultwarden/vw-data
vaultwarden_user: vaultwarden
vaultwarden_group: vaultwarden
# Docker settings
vaultwarden_container_name: vaultwarden
vaultwarden_network_name: vaultwarden_net
# Security settings
vaultwarden_signups_allowed: false
vaultwarden_signups_verify: false
vaultwarden_organizations_allowed: true
vaultwarden_attachments_allowed: true
vaultwarden_send_allowed: true
vaultwarden_emergency_access_allowed: true
vaultwarden_websocket_enabled: true
vaultwarden_events_enabled: true
vaultwarden_events_days_retain: 90
# Rate limiting
vaultwarden_login_ratelimit_max_burst: 5
vaultwarden_login_ratelimit_seconds: 120
vaultwarden_admin_ratelimit_max_burst: 3
vaultwarden_admin_ratelimit_seconds: 300
# Performance
vaultwarden_num_workers: 4
vaultwarden_request_size_limit: 10485760
# Logging
vaultwarden_log_level: info
vaultwarden_log_to_file: true
# Backup settings
vaultwarden_backup_enabled: true
vaultwarden_backup_dir: /backup/vaultwarden
vaultwarden_backup_retention_days: 30
vaultwarden_backup_cron_hour: 2
vaultwarden_backup_cron_minute: 0
# Nginx settings
vaultwarden_nginx_enabled: true
vaultwarden_nginx_ssl_enabled: true
vaultwarden_nginx_ssl_cert_path: /etc/nginx/ssl
vaultwarden_nginx_rate_limit_rate: "10r/s"
vaultwarden_nginx_rate_limit_burst: 20
# Monitoring
vaultwarden_healthcheck_enabled: true
vaultwarden_healthcheck_interval: 60s
playbooks/deploy.yml---
# =============================================================================
# Vaultwarden Deployment Playbook
# =============================================================================
- name: Deploy Vaultwarden Password Server
hosts: vaultwarden
become: true
gather_facts: true
vars:
# Generate secure tokens if not provided
vaultwarden_admin_token: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=48') }}"
vaultwarden_db_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
pre_tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- vaultwarden_domain is defined
- vaultwarden_domain | length > 0
fail_msg: "vaultwarden_domain variable is required"
- name: Update apt cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Update dnf cache (RHEL)
ansible.builtin.dnf:
update_cache: true
when: ansible_os_family == "RedHat"
roles:
- role: docker
- role: vaultwarden
- role: nginx
when: vaultwarden_nginx_enabled
post_tasks:
- name: Display deployment summary
ansible.builtin.debug:
msg: |
=============================================
Vaultwarden Deployment Complete
=============================================
Domain: {{ vaultwarden_domain }}
Version: {{ vaultwarden_version }}
Admin Panel: https://{{ vaultwarden_domain }}/admin
IMPORTANT: Save the admin token securely!
Admin Token: {{ vaultwarden_admin_token }}
=============================================
no_log: true
- name: Verify service is running
ansible.builtin.command:
cmd: docker compose ps
chdir: "{{ vaultwarden_app_root }}"
register: compose_status
changed_when: false
- name: Display service status
ansible.builtin.debug:
var: compose_status.stdout_lines
roles/docker/tasks/main.yml---
# =============================================================================
# Docker Installation Role
# =============================================================================
- name: Install Docker prerequisites
ansible.builtin.package:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
when: ansible_os_family == "Debian"
- name: Add Docker GPG key (Debian/Ubuntu)
ansible.builtin.apt_key:
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
state: present
when: ansible_os_family == "Debian"
- name: Add Docker repository (Debian/Ubuntu)
ansible.builtin.apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
state: present
filename: docker
when: ansible_os_family == "Debian"
- name: Install Docker packages
ansible.builtin.package:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
notify: Restart Docker
- name: Enable and start Docker service
ansible.builtin.systemd:
name: docker
state: started
enabled: true
- name: Create Docker group
ansible.builtin.group:
name: docker
state: present
- name: Add application user to Docker group
ansible.builtin.user:
name: "{{ vaultwarden_user }}"
groups: docker
append: true
state: present
roles/vaultwarden/tasks/main.yml---
# =============================================================================
# Vaultwarden Application Role
# =============================================================================
- name: Create application user
ansible.builtin.user:
name: "{{ vaultwarden_user }}"
group: "{{ vaultwarden_group }}"
system: true
shell: /usr/sbin/nologin
create_home: false
- name: Create application directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ vaultwarden_user }}"
group: "{{ vaultwarden_group }}"
mode: "0750"
loop:
- "{{ vaultwarden_app_root }}"
- "{{ vaultwarden_data_dir }}"
- "{{ vaultwarden_data_dir }}/attachments"
- "{{ vaultwarden_data_dir }}/icons"
- "{{ vaultwarden_data_dir }}/sends"
- "{{ vaultwarden_backup_dir }}"
- name: Generate .env file
ansible.builtin.template:
src: .env.j2
dest: "{{ vaultwarden_app_root }}/.env"
owner: "{{ vaultwarden_user }}"
group: "{{ vaultwarden_group }}"
mode: "0600"
vars:
admin_token: "{{ vaultwarden_admin_token }}"
db_password: "{{ vaultwarden_db_password }}"
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ vaultwarden_app_root }}/docker-compose.yml"
owner: "{{ vaultwarden_user }}"
group: "{{ vaultwarden_group }}"
mode: "0644"
- name: Deploy backup script
ansible.builtin.template:
src: backup.sh.j2
dest: "{{ vaultwarden_app_root }}/backup.sh"
owner: "{{ vaultwarden_user }}"
group: "{{ vaultwarden_group }}"
mode: "0750"
when: vaultwarden_backup_enabled
- name: Configure backup cron job
ansible.builtin.cron:
name: "Vaultwarden backup"
hour: "{{ vaultwarden_backup_cron_hour }}"
minute: "{{ vaultwarden_backup_cron_minute }}"
job: "{{ vaultwarden_app_root }}/backup.sh"
user: root
when: vaultwarden_backup_enabled
- name: Start Vaultwarden with Docker Compose
ansible.builtin.command:
cmd: docker compose up -d
chdir: "{{ vaultwarden_app_root }}"
register: compose_up
changed_when: "'Creating' in compose_up.stdout or 'Recreating' in compose_up.stdout"
- name: Wait for Vaultwarden to be healthy
ansible.builtin.wait_for:
timeout: 30
delegate_to: localhost
roles/nginx/tasks/main.yml---
# =============================================================================
# Nginx Reverse Proxy Role
# =============================================================================
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
- name: Create Nginx configuration directory
ansible.builtin.file:
path: /etc/nginx/conf.d
state: directory
mode: "0755"
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/conf.d/vaultwarden.conf
mode: "0644"
notify: Reload Nginx
- name: Create SSL directory
ansible.builtin.file:
path: "{{ vaultwarden_nginx_ssl_cert_path }}"
state: directory
mode: "0700"
when: vaultwarden_nginx_ssl_enabled
- name: Enable and start Nginx
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
templates/.env.j2# =============================================================================
# Vaultwarden Environment Configuration
# Generated by Ansible - Do not edit manually
# =============================================================================
# -----------------------------------------------------------------------------
# Core Settings
# -----------------------------------------------------------------------------
DOMAIN=https://{{ vaultwarden_domain }}
DATABASE_URL=/data/db.sqlite3
ADMIN_TOKEN={{ admin_token }}
# -----------------------------------------------------------------------------
# Access Control
# -----------------------------------------------------------------------------
SIGNUPS_ALLOWED={{ vaultwarden_signups_allowed | lower }}
SIGNUPS_VERIFY={{ vaultwarden_signups_verify | lower }}
INVITATIONS_ALLOWED=true
ORGANIZATIONS_ALLOWED={{ vaultwarden_organizations_allowed | lower }}
# -----------------------------------------------------------------------------
# Security
# -----------------------------------------------------------------------------
LOGIN_RATELIMIT_MAX_BURST={{ vaultwarden_login_ratelimit_max_burst }}
LOGIN_RATELIMIT_SECONDS={{ vaultwarden_login_ratelimit_seconds }}
ADMIN_RATELIMIT_MAX_BURST={{ vaultwarden_admin_ratelimit_max_burst }}
ADMIN_RATELIMIT_SECONDS={{ vaultwarden_admin_ratelimit_seconds }}
IP_HEADER=X-Forwarded-For
# -----------------------------------------------------------------------------
# WebSocket
# -----------------------------------------------------------------------------
WEBSOCKET_ENABLED={{ vaultwarden_websocket_enabled | lower }}
WEBSOCKET_PORT=3012
# -----------------------------------------------------------------------------
# Features
# -----------------------------------------------------------------------------
ATTACHMENTS_ALLOWED={{ vaultwarden_attachments_allowed | lower }}
SEND_ALLOWED={{ vaultwarden_send_allowed | lower }}
EMERGENCY_ACCESS_ALLOWED={{ vaultwarden_emergency_access_allowed | lower }}
EVENTS_ENABLED={{ vaultwarden_events_enabled | lower }}
EVENTS_DAYS_RETAIN={{ vaultwarden_events_days_retain }}
# -----------------------------------------------------------------------------
# Performance
# -----------------------------------------------------------------------------
NUM_WORKERS={{ vaultwarden_num_workers }}
REQUEST_SIZE_LIMIT={{ vaultwarden_request_size_limit }}
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
LOG_LEVEL={{ vaultwarden_log_level }}
LOG_TO_FILE={{ vaultwarden_log_to_file | lower }}
templates/docker-compose.yml.j2---
# =============================================================================
# Vaultwarden Docker Compose Configuration
# Generated by Ansible - Do not edit manually
# Version: {{ vaultwarden_version }}
# =============================================================================
services:
vaultwarden:
image: ghcr.io/dani-garcia/vaultwarden:{{ vaultwarden_version }}
container_name: {{ vaultwarden_container_name }}
restart: unless-stopped
user: "1000:1000"
# Security hardening
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
volumes:
- {{ vaultwarden_data_dir }}:/data:Z
- {{ vaultwarden_data_dir }}/attachments:/data/attachments:Z
- {{ vaultwarden_data_dir }}/icons:/data/icons:Z
- {{ vaultwarden_data_dir }}/sends:/data/sends:Z
env_file:
- .env
networks:
- {{ vaultwarden_network_name }}
labels:
- "com.vaultwarden.version={{ vaultwarden_version }}"
- "com.vaultwarden.domain={{ vaultwarden_domain }}"
networks:
{{ vaultwarden_network_name }}:
driver: bridge
name: {{ vaultwarden_network_name }}
templates/nginx.conf.j2# =============================================================================
# Vaultwarden Nginx Reverse Proxy Configuration
# Generated by Ansible
# Domain: {{ vaultwarden_domain }}
# =============================================================================
limit_req_zone $binary_remote_addr zone=vaultwarden_limit:10m rate={{ vaultwarden_nginx_rate_limit_rate }};
# HTTP β HTTPS redirect
server {
listen 80;
server_name {{ vaultwarden_domain }};
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 {{ vaultwarden_domain }};
ssl_protocols TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
{% if vaultwarden_nginx_ssl_enabled %}
ssl_certificate {{ vaultwarden_nginx_ssl_cert_path }}/fullchain.pem;
ssl_certificate_key {{ vaultwarden_nginx_ssl_cert_path }}/privkey.pem;
{% endif %}
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss://$host;" always;
location / {
limit_req zone=vaultwarden_limit burst={{ vaultwarden_nginx_rate_limit_burst }} nodelay;
proxy_pass http://127.0.0.1:8000;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub {
proxy_pass http://127.0.0.1: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;
}
location /admin {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 172.16.0.0/12;
deny all;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
templates/backup.sh.j2#!/bin/bash
# =============================================================================
# Vaultwarden Backup Script
# Generated by Ansible
# =============================================================================
set -e
BACKUP_DIR="{{ vaultwarden_backup_dir }}"
DATE=$(date +%Y%m%d_%H%M%S)
DATA_DIR="{{ vaultwarden_data_dir }}"
mkdir -p "${BACKUP_DIR}"
# Create compressed backup
tar -czf "${BACKUP_DIR}/vaultwarden_${DATE}.tar.gz" \
-C "$(dirname "${DATA_DIR}")" \
"$(basename "${DATA_DIR}")"
# Retain only last {{ vaultwarden_backup_retention_days }} days
find "${BACKUP_DIR}" -name "vaultwarden_*.tar.gz" -mtime +{{ vaultwarden_backup_retention_days }} -delete
echo "Backup completed: ${BACKUP_DIR}/vaultwarden_${DATE}.tar.gz"
roles/vaultwarden/handlers/main.yml---
- name: Restart Vaultwarden
ansible.builtin.command:
cmd: docker compose restart
chdir: "{{ vaultwarden_app_root }}"
- name: Reload Vaultwarden
ansible.builtin.command:
cmd: docker compose reload
chdir: "{{ vaultwarden_app_root }}"
roles/nginx/handlers/main.yml---
- name: Reload Nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: Restart Nginx
ansible.builtin.systemd:
name: nginx
state: restarted
playbooks/update.yml---
- name: Update Vaultwarden
hosts: vaultwarden
become: true
vars:
vaultwarden_new_version: "1.35.3"
tasks:
- name: Backup current data
ansible.builtin.command:
cmd: "{{ vaultwarden_app_root }}/backup.sh"
register: backup_result
- name: Pull new image
ansible.builtin.command:
cmd: docker compose pull
chdir: "{{ vaultwarden_app_root }}"
- name: Update Docker Compose configuration
ansible.builtin.lineinfile:
path: "{{ vaultwarden_app_root }}/docker-compose.yml"
regexp: 'image: ghcr.io/dani-garcia/vaultwarden:'
line: " image: ghcr.io/dani-garcia/vaultwarden:{{ vaultwarden_new_version }}"
- name: Recreate Vaultwarden container
ansible.builtin.command:
cmd: docker compose up -d --force-recreate
chdir: "{{ vaultwarden_app_root }}"
- name: Verify service is running
ansible.builtin.command:
cmd: docker compose ps
chdir: "{{ vaultwarden_app_root }}"
register: status
changed_when: false
- name: Display update status
ansible.builtin.debug:
var: status.stdout_lines
playbooks/backup.yml---
- name: Manual Backup
hosts: vaultwarden
become: true
tasks:
- name: Run backup script
ansible.builtin.command:
cmd: "{{ vaultwarden_app_root }}/backup.sh"
register: backup_output
- name: Display backup result
ansible.builtin.debug:
var: backup_output.stdout_lines
# Navigate to project directory
cd vaultwarden-ansible
# Deploy to all vaultwarden hosts
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml
# Deploy with vaultwarden_admin_token specified
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml \
-e vaultwarden_admin_token="your-secure-token"
# Update to new version
ansible-playbook -i inventory/hosts.yml playbooks/update.yml \
-e vaultwarden_new_version="1.35.3"
ansible-playbook -i inventory/hosts.yml playbooks/backup.yml
Any questions?
Feel free to contact us. Find all contact information on our contact page.