Self-hosting Vaultwarden: Step-by-Step Guide with Docker, Caddy, and Fail2Ban
Host your own password manager, Vaultwarden, securely and efficiently. Install Vaultwarden as a Docker container and secure it with Fail2Ban.

Table of Contents
- What is Vaultwarden?
- Prerequisites
- Step 1: Server Preparation
- Step 2: Create Docker Network
- Step 3: Create Vaultwarden Directory Structure
- Step 4: Create Docker Compose File for Vaultwarden
- Step 5: Start Vaultwarden
- Step 6: Configure Reverse Proxy (Caddy)
- Step 7: First Access and Account Creation
- Step 8: Secure Admin Area and Disable Registration
- Step 9: Configure Logging for Vaultwarden
- Step 10: Set Up Fail2Ban for Additional Security
- Step 11: Create Fail2Ban Configuration
- Step 12: Start and Test Fail2Ban
- Step 13: Backup Strategy
- Summary
What is Vaultwarden?
Vaultwarden is an alternative server for the Bitwarden ecosystem, written in the Rust programming language. This makes it particularly performant and resource-friendly – ideal for a home server or a small VPS. You can use all official Bitwarden clients (browser extensions, desktop apps, mobile apps) with it.
Prerequisites
Before we start, make sure you have the following:
- A Server: A server with a common Linux operating system. I’m using Ubuntu 24.04 here, but other distributions work similarly.
- Docker and Docker Compose: Must be installed on the server.
- A Domain: A domain or subdomain that points to the public IP address of your server.
- A Reverse Proxy: A working reverse proxy. In this tutorial, we’ll use Caddy. Other proxies like Nginx Proxy Manager or Traefik are also possible but require adjusted configuration.
- (Optional) Pengolin: If you’re using a home server without a fixed public IPv4 address, check out Pengolin (or a similar tool/guide) to make your server accessible.
Step 1: Server Preparation
Open a terminal and connect to your server via SSH.
ssh your_user@YOUR_SERVER_IP
Replace your_user
and YOUR_SERVER_IP
accordingly.
Domain Check: Ensure that your domain correctly points to your server’s IP.
ping password.yourdomain.com
Replace password.yourdomain.com
with your desired domain. The output should show your server’s IP address. If not, check your DNS settings and wait for propagation if necessary.
Docker Check: Check if Docker and Docker Compose are installed.
docker -v
docker compose version
You should see the version numbers. If not, install Docker and Docker Compose according to the official documentation for your operating system.
Step 2: Create Docker Network
We need a shared Docker network so that our reverse proxy (Caddy) can communicate with the Vaultwarden container.
sudo docker network create proxy
We’ll call the network proxy
. You can verify the name with:
sudo docker network ls
Step 3: Create Vaultwarden Directory Structure
Create a directory for the Vaultwarden configuration and persistent data.
mkdir ~/vaultwarden
cd ~/vaultwarden
mkdir vw-data
The vw-data
folder will later contain the encrypted database, attachments, and settings of Vaultwarden.
Step 4: Create Docker Compose File for Vaultwarden
Create a docker-compose.yml
file in the ~/vaultwarden
directory:
nano docker-compose.yml
Paste the following content and adapt it:
version: "3"
services:
vaultwarden:
image: vaultwarden/server:1.31.0 # Use a specific tag instead of 'latest' (Note: Original German text mentioned 1.33.2 in the adjustment notes, but used 1.31.0 here. Sticking to 1.31.0 as per the code block.)
container_name: vaultwarden
restart: always
environment:
# - WEBSOCKET_ENABLED=true # Disable WebSocket if not needed or if causing issues
# Admin token will be added here later
# ADMIN_TOKEN: 'YOUR_SECURE_ADMIN_TOKEN_HASH'
# Set timezone (Important for logs and Fail2Ban)
TZ: "Europe/Berlin" # Adjust this to your time zone
volumes:
- ./vw-data:/data
# Mounts for time zone (Important for correct log times -> Fail2Ban)
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
# Network for connecting to the reverse proxy
networks:
- proxy
# Run as non-root user (optional but recommended)
# Find your User/Group ID with 'id' in the terminal
# user: '1000:1000' # Replace 1000:1000 with your UID:GID
networks:
proxy:
external: true # We use the previously created external network
Important Adjustments:
- Image Tag: I used
vaultwarden/server:1.31.0
. Check on GitHub for the latest stable version and replace the tag accordingly. Using a specific tag prevents unexpected updates and facilitates manual, controlled updates. (The German text mentioned updating to1.33.2
in this description, but the code used1.31.0
. Adjust the tag in the code if you prefer1.33.2
or the latest.) - Ports: We remove the
ports
section as access should only occur via the reverse proxy. - Network: We add the container to the
proxy
network and declare it asexternal
. - User (Optional): To increase security, you can run the container under your normal user. Find your User ID (UID) and Group ID (GID) with the
id
command on your server and add theuser: 'UID:GID'
line (uncomment it). Ensure your user has write permissions on the./vw-data
directory (e.g.,sudo chown YOUR_UID:YOUR_GID vw-data
). - Time zone (TZ, Volumes): The
TZ
variable and thelocaltime
/timezone
volumes are important so that the logs in the container have the correct time. This is crucial for Fail2Ban later. Adjust'Europe/Berlin'
to your time zone.
Save the file (Ctrl+X, then Y, then Enter in nano
).
Step 5: Start Vaultwarden
Start the Vaultwarden container in the background:
sudo docker compose up -d
Docker will now download the Vaultwarden image (if not already present) and start the container.
Step 6: Configure Reverse Proxy (Caddy)
Now let’s configure Caddy so Vaultwarden is accessible via your domain and automatically gets an SSL certificate. Edit your Caddyfile:
password.yourdomain.com {
# Add header for real client IP (important for logs/Fail2Ban)
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
# Optional: Adjust Caddy log level
log {
level WARN
}
}
# Add other domains here if needed
Important Points:
- Replace
password.yourdomain.com
with your domain. vaultwarden:80
refers to the container name (vaultwarden
) and the internal port Vaultwarden listens on (default is 80).header_up X-Real-IP {remote_host}
is crucial so that Vaultwarden sees the client’s real IP address and not the Caddy container’s IP. This is important for logs and Fail2Ban.- Adjust the
log level
if necessary to reduce Caddy’s log output.
Reload the Caddy configuration. If Caddy is running as a Docker container (e.g., named caddy
), it might be:
sudo docker exec caddy caddy reload -c /etc/caddy/Caddyfile
(Adjust the path to the Caddyfile inside the container if needed).
Step 7: First Access and Account Creation
Open your browser and navigate to https://password.yourdomain.com
. You should see the Vaultwarden web interface. Click on “Create Account”, enter your details, and create your first user account.
You can now log in and use the password manager!
Step 8: Secure Admin Area and Disable Registration
By default, anyone can create an account on your Vaultwarden instance. We want to change that. For this, we need access to the admin area, which is accessible via https://password.yourdomain.com/admin
. This is protected by an admin token, which we first need to generate.
Generate Admin Token:
We use argon2
to hash the admin password. Install it first (example for Debian/Ubuntu):
sudo apt update && sudo apt install argon2 -y
Now generate the hash for your desired admin password (replace YourSecureAdminPassword
):
echo -n 'YourSecureAdminPassword' | argon2 "$(openssl rand -base64 16)" -e -id -k 65536 -t 3 -p 4 | sed 's/\$/\$\$/g'
Copy the entire output, starting with $$argon2id$$...
.
Add Admin Token to docker-compose.yml
:
Open the docker-compose.yml
again:
nano docker-compose.yml
Add the ADMIN_TOKEN
variable under environment
or uncomment it and paste the hash:
# ... (other services or settings)
services:
vaultwarden:
# ... (other Vaultwarden settings)
environment:
# ... (other environment variables like TZ)
ADMIN_TOKEN: YOUR_COPIED_ARGON2_HASH # Paste the copied hash here
# ... (rest of Vaultwarden configuration)
# ... (networks etc.)
Save the file and restart Vaultwarden for the change to take effect:
sudo docker compose down
sudo docker compose up -d
Disable Registration:
- Go to
https://password.yourdomain.com/admin
. - Enter your original admin password (the one you put into the
echo
command above, not the hash). - Go to the “General Settings”.
- Uncheck “Allow new signups”.
- Click “Save”.
Now try (e.g., in a private browser window) to create a new account. An error message should appear stating that registration is disabled.
In the admin panel, you can also configure SMTP settings for sending emails (useful for invitations or password resets) and manage users.
Step 9: Configure Logging for Vaultwarden
To be able to log failed login attempts (which we need for Fail2Ban), we add logging options to the docker-compose.yml
.
Open the docker-compose.yml
:
nano docker-compose.yml
Add the following lines under environment
in the vaultwarden
service:
# ...
services:
vaultwarden:
# ...
environment:
# ... (TZ, ADMIN_TOKEN)
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn" # Logs warnings and errors (incl. failed logins)
# ...
# ...
# ...
Save the file and restart Vaultwarden:
sudo docker compose down
sudo docker compose up -d
Now, failed logins will be written to the vw-data/vaultwarden.log
file. You can test this by trying to log in with an incorrect password and then checking the log:
tail -f ~/vaultwarden/vw-data/vaultwarden.log
You should see a line like [WARN] Wrong password provided for user...
or [INFO] Login request failed...
, followed by the client’s IP address (thanks to X-Real-IP
in the Caddy setup).
Step 10: Set Up Fail2Ban for Additional Security
Fail2Ban monitors log files and bans IP addresses that make repeated failed login attempts. We’ll use the popular Fail2Ban Docker image from CrazyMax.
Directory Structure for Fail2Ban:
In the ~/vaultwarden
directory, create a folder for Fail2Ban data:
cd ~/vaultwarden
mkdir fail2ban-data
Add Fail2Ban to docker-compose.yml
:
Open the docker-compose.yml
:
nano docker-compose.yml
Add a new service for Fail2Ban:
services:
vaultwarden:
# ... (Your Vaultwarden configuration from above) ...
fail2ban:
image: crazymax/fail2ban:latest
container_name: fail2ban
restart: always
# Important: Host network mode allows Fail2Ban to modify host iptables
network_mode: host
# Necessary capabilities for network administration
cap_add:
- NET_ADMIN
- NET_RAW
environment:
# Set the timezone matching your host and Vaultwarden
TZ: "Europe/Berlin"
# Optional: Set the log level for Fail2Ban itself
F2B_LOG_LEVEL: "INFO"
volumes:
# Persistent data for Fail2Ban (ban database, etc.)
- ./fail2ban-data:/data
# Vaultwarden log file (mount read-only)
- ./vw-data/vaultwarden.log:/var/log/vaultwarden/vaultwarden.log:ro
networks:
proxy:
external: true
Important Points:
network_mode: host
: Allows Fail2Ban to directly access the host system’s network configuration andiptables
to block IPs.cap_add: [NET_ADMIN, NET_RAW]
: Gives the container the necessary privileges for this.volumes
: We mount thefail2ban-data
directory for configuration and the database, as well as thevaultwarden.log
(read-only!). The path/var/log/vaultwarden/vaultwarden.log
inside the container is the path we will use later in the Fail2Ban configuration.TZ
: Make sure the time zone here matches the host’s and the Vaultwarden container’s!
Save the file.
Step 11: Create Fail2Ban Configuration
We need to tell Fail2Ban what to look for in the log file (Filter) and what to do when it finds something (Jail).
Change to the Fail2Ban data directory and create the necessary subfolders:
cd ~/vaultwarden/fail2ban-data
mkdir filter.d jail.d action.d
Create Filters:
Create a filter file for normal Vaultwarden logins:
nano filter.d/vaultwarden.local
Paste the following content:
[Includes]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
ignoreregex =
(Note: The exact failregex
might need slight adjustments depending on your specific Vaultwarden log output format. Test with your logs.)
Save the file.
Create a filter file for failed admin logins:
nano filter.d/vaultwarden-admin.local
Paste the following content:
[Includes]
before = common.conf
[Definition]
failregex = ^.*?Invalid admin token\. IP: <ADDR>.*$
# ignoreregex =
(Note: Check your admin log format for the exact message and adjust failregex
if needed. Use <ADDR>
to capture the IP.)
Save the file.
Create Jail:
Create a central jail configuration file:
nano jail.d/vaultwarden.local
Paste the following content and adjust the values as needed:
[vaultwarden]
enabled = true
# Path to the log file as mounted inside the Fail2Ban container
logpath = /var/log/vaultwarden/vaultwarden.log
# Filter used (name of the .local file without extension)
filter = vaultwarden
# Action using iptables, targeting the DOCKER-USER chain often used for host network mode containers
action = iptables-allports[name=vaultwarden, chain=DOCKER-USER]
# Specify ports usually exposed by the reverse proxy
ports = http,https # Corresponds to 80, 443
# Maximum number of failed attempts
maxretry = 5
# Time window for failed attempts (e.g., 1h = 1 hour)
findtime = 1h
# Ban duration (e.g., 24h = 24 hours, 1d = 1 day, 1w = 1 week)
bantime = 24h
[vaultwarden-admin]
enabled = true
logpath = /var/log/vaultwarden/vaultwarden.log
filter = vaultwarden-admin
action = iptables-allports[name=vaultwarden-admin, chain=DOCKER-USER]
ports = http,https
# Stricter rules for admin access
maxretry = 3
findtime = 10h
bantime = 7d # 7 day ban
Important Parameters:
enabled = true
: Activates the jail.logpath
: Must exactly match the path where the log file is accessible inside the Fail2Ban container.filter
: The name of the corresponding filter file infilter.d
(without.local
).action = iptables-allports[name=..., chain=DOCKER-USER]
: Usesiptables
to block all ports for the offending IP, placing the rule in theDOCKER-USER
chain. This chain is commonly used and processed before Docker’s own rules when usingnetwork_mode: host
. Check youriptables -L
if this doesn’t work; you might need a different chain likeINPUT
.ports = http,https
: Specifies the ports relevant for the ban action (usually the web ports).maxretry
: Number of allowed failed attempts.findtime
: Time window during which themaxretry
attempts must occur to trigger a ban.bantime
: Duration of the ban in seconds (suffixes likem
,h
,d
,w
are possible).
Adjust maxretry
, findtime
, and bantime
according to your security requirements. Save the file.
Step 12: Start and Test Fail2Ban
Go back to the main directory (cd ~/vaultwarden
) and restart both containers so all changes (Compose file, Fail2Ban configs) are loaded:
sudo docker compose down
sudo docker compose up -d
Check the Fail2Ban logs:
sudo docker compose logs -f fail2ban
You should see messages indicating that the jails vaultwarden
and vaultwarden-admin
were found and started, and eventually something like Server ready
.
Testing:
-
Open your Vaultwarden login page (
https://password.yourdomain.com
). -
Intentionally enter the wrong password 5 times (or according to your
maxretry
value for[vaultwarden]
). -
On the next attempt (or possibly before), the page should no longer load (timeout or connection error).
-
Check the status of the jail in the Fail2Ban container (replace
YOUR_CURRENT_IP
with the IP you tested from):sudo docker compose exec fail2ban fail2ban-client status vaultwarden
You should see your IP listed under “Currently banned”.
-
Unban (if you locked yourself out):
sudo docker compose exec fail2ban fail2ban-client set vaultwarden unbanip YOUR_CURRENT_IP
After this, you should be able to load the page again.
Repeat the test for the admin area (/admin
) with an incorrect admin token if necessary to test the vaultwarden-admin
jail (Note: maxretry
is lower here).
Step 13: Backup Strategy
A self-hosted service is only as good as its backup! Make sure you create backups regularly. Important data includes:
- Vaultwarden Data: The entire content of the
~/vaultwarden/vw-data
folder. It contains the encrypted database, attachments, settings, and logs. This is the most critical part! - Fail2Ban Data: The entire content of the
~/vaultwarden/fail2ban-data
folder. It contains your configuration and the database of banned IPs. - Docker Compose File: The
~/vaultwarden/docker-compose.yml
. - Caddy Configuration: Your
Caddyfile
and potentially data generated by Caddy (certificates, etc., often under/var/lib/caddy
or a mapped volume).
There are many backup tools. Duplicati is a common option. Ensure your backups are stored in a secure, external location and tested regularly!
Summary
Congratulations! You have successfully:
- Set up Vaultwarden with Docker Compose.
- Used a specific image tag and configured the container to run as a non-root user (optional).
- Set up Caddy as a reverse proxy with SSL and the Real-IP header.
- Secured the admin area with a token and disabled public registration.
- Configured logging for Vaultwarden.
- Set up and tested Fail2Ban for automatic blocking of attackers.
- Recognized the importance of a backup strategy.
Your own, secure password manager is now running on your server! You can now configure the Bitwarden browser extensions and apps with your server URL (https://password.yourdomain.com
) and manage your passwords securely.
I hope this blog post helps you install Vaultwarden! Let me know if you have any adjustments or additions.