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.

Self-hosting Vaultwarden: Step-by-Step Guide with Docker, Caddy, and Fail2Ban hero image

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:

  1. A Server: A server with a common Linux operating system. I’m using Ubuntu 24.04 here, but other distributions work similarly.
  2. Docker and Docker Compose: Must be installed on the server.
  3. A Domain: A domain or subdomain that points to the public IP address of your server.
  4. 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.
  5. (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:

  1. 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 to 1.33.2 in this description, but the code used 1.31.0. Adjust the tag in the code if you prefer 1.33.2 or the latest.)
  2. Ports: We remove the ports section as access should only occur via the reverse proxy.
  3. Network: We add the container to the proxy network and declare it as external.
  4. 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 the user: '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).
  5. Time zone (TZ, Volumes): The TZ variable and the localtime/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:

  1. Replace password.yourdomain.com with your domain.
  2. vaultwarden:80 refers to the container name (vaultwarden) and the internal port Vaultwarden listens on (default is 80).
  3. 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.
  4. 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:

  1. Go to https://password.yourdomain.com/admin.
  2. Enter your original admin password (the one you put into the echo command above, not the hash).
  3. Go to the “General Settings”.
  4. Uncheck “Allow new signups”.
  5. 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:

  1. network_mode: host: Allows Fail2Ban to directly access the host system’s network configuration and iptables to block IPs.
  2. cap_add: [NET_ADMIN, NET_RAW]: Gives the container the necessary privileges for this.
  3. volumes: We mount the fail2ban-data directory for configuration and the database, as well as the vaultwarden.log (read-only!). The path /var/log/vaultwarden/vaultwarden.log inside the container is the path we will use later in the Fail2Ban configuration.
  4. 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 in filter.d (without .local).
  • action = iptables-allports[name=..., chain=DOCKER-USER]: Uses iptables to block all ports for the offending IP, placing the rule in the DOCKER-USER chain. This chain is commonly used and processed before Docker’s own rules when using network_mode: host. Check your iptables -L if this doesn’t work; you might need a different chain like INPUT.
  • 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 the maxretry attempts must occur to trigger a ban.
  • bantime: Duration of the ban in seconds (suffixes like m, 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:

  1. Open your Vaultwarden login page (https://password.yourdomain.com).

  2. Intentionally enter the wrong password 5 times (or according to your maxretry value for [vaultwarden]).

  3. On the next attempt (or possibly before), the page should no longer load (timeout or connection error).

  4. 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”.

  5. 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:

  1. 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!
  2. Fail2Ban Data: The entire content of the ~/vaultwarden/fail2ban-data folder. It contains your configuration and the database of banned IPs.
  3. Docker Compose File: The ~/vaultwarden/docker-compose.yml.
  4. 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.

Share this post:

This website uses cookies. These are necessary for the functionality of the website. You can find more information in the privacy policy