
Run Vaultwarden and Caddy on your Linux Server with docker-compose
Vaultwarden is a very light, easy to use and very well documented alternative implementation of the Bitwarden Client API. It is perfect if you struggle with the complex Bitwarden installation but want to self-host your own password management server and connect your Bitwarden Clients which are installed on your computer or mobile device. In this documentation I describe the steps to configure and run Vaultwarden on a Ubuntu Linux 22.04 using docker-compose services. In parallel you should read the Vaultwarden Wiki to understand the complete background.
Prepare your Server
Before we start we need to prepare the server. In this step we create the environment to manage the vaultwarden instance. Login to your server using your standard User (not root).
Basic requirements
Login to your server with SSH and authenticate with keys. Never use simple password authentication. You must create a private and a public SSH key-pair on your Host Machine and copy only the public SSH key to your Remote Server. Keep your private key safe on your Host machine. Then copy your public key to your Remote Server and configure your ssh-deamon on your Remote Server. Disable password authentication and root login in your configuration on your Remote Server. How all this works is very good explained on Linuxize.com.
You must ensure that SSL certificates for your server are installed. I use free LetsEncrypt certificates and certbot to install and renew my certificates on the system. Therefore pls. read on my Digitaldocblog in the Article SSL Certificates with Lets Encrypt and certbot on a Linux Server . Here you find a very detailed description of how you can do this .
You must ensure that docker and docker-compose is installed on your system. Therefore pls. read on my Digitaldocblog a very detailed description of how you can do this in the Article Containerize a nodejs app with nginx. You should read the following chapters:
- Prepare the System for secure Downloads from Docker
- Install Docker from Docker Resources
- Install standalone docker-compose from Docker Resources
Make sure that your server is running behind a firewall. In my case I have a virtual server and I am responsible for server security. Therefore I install and configure a firewall on my system.
Before you configure the firewall be sure that your ssh service is running and on which port your ssh service is running. This is important to know because you don’t want to lock yourself out. First you check if the ssh service is running with systemctl and then on which port ssh is running using netstat. If netstat is not installed on your system you can do this with apt.
#control ssh status
sudo systemctl status ssh
#check ssh port
sudo netstat -tulnp | grep ssh
#check if net-tools (include netstat) are installed
which netstat
#install net-tools only in case not installed
sudo apt install net-tools
I install ufw (uncomplicated firewall) on my server and configure it to provide only SSH and HTTPS to the outside world.
# check if ufw is installed
ufw version
which ufw
#install ufw if not installed
sudo apt install ufw
#open SSH and HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 443
#Default rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
#Start the firewall
sudo ufw enable
#Check the firewall status
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
443 ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
443 (v6) ALLOW IN Anywhere (v6)
Create new user vaultwarden
You are logged in with your standarduser. From the home directory of the standarduser you create the user vaultwarden and create the hidden directory /home/vaultwarden/.ssh. Then copy the authorized_keys file from your .ssh directory into the new created .ssh directory of the new user vaultwarden and set the owner and permissions.
The new user vaultwarden should be in sudo group to perform commands under root using sudo. And the user vaultwarden should be in docker group to perform docker commands without using sudo.
#logged-in with standard user and create new user
sudo adduser vaultwarden
#create hidden .ssh directory in new users home directory
sudo mkdir /home/vaultwarden/.ssh
#copy authorized_keys file to enable ssh key login for new user
cd /home/standarduser/.ssh
sudo cp authorized_keys /home/vaultwarden/.ssh
#set the owner vaultwarden and permissions
sudo chown -R vaultwarden:vaultwarden /home/vaultwarden/.ssh
sudo chmod 700 /home/vaultwarden/.ssh
sudo chmod 600 /home/vaultwarden/.ssh/authorized_keys
#check permissions
ls -al /home/vaultwarden
drwx-- vaultwarden vaultwarden 4096 May 11 14:20 .ssh
ls -l /home/vaultwarden/.ssh
-rw-- vaultwarden vaultwarden 400 May 11 14:20 authorized_keys
#add user vaultwarden to sudo- and docker group
sudo usermod -aG sudo vaultwarden
sudo usermod -aG docker vaultwarden
#check vaultwarden groups (3 groups: vaultwarden sudo docker)
sudo groups vaultwarden
vaultwarden : vaultwarden sudo docker
Create /opt/vaultwarden directory
Login with the new user vaultwarden. Stay logged in as vaultwarden for the next steps. Do not perform the next steps or the installation of the vaultwarden server with root or any other user.
After you are logged in with vaultwarden you create a new directory /opt/vaultwarden. This is the runtime directory of your vaultwarden application and the place from where the docker containers will be started.
sudo mkdir /opt/vaultwarden
sudo chmod -R 700 /opt/vaultwarden
ls -l /opt/vaultwarden
drwx--vaultwarden vaultwarden 4096 Mai 16 07:06 vaultwarden
Then change into /opt/vaultwarden. You create the /opt/vaultwarden/vw-data directory which is the host directory for the docker containers. One of these containers will be started under the container name vaultwarden. This container vaultwarden run with root privileges and write into this host directory /opt/vaultwarden/vw-data.
mkdir /opt/vaultwarden/vw-data
ls -l /opt/vaultwarden
drwxrwxr-x 6 vaultwarden vaultwarden 4096 Mai 15 15:54 vw-data
`
Create /opt/vaultwarden/certs and copy your SSL certificates
The container vaultwarden is the web application where you can log-in and manage your passwords. As we will se below the container will be started under the user vaultwarden in /opt/vaultwarden and run with root privileges behind a reverse proxy server. As reverse proxy I will use Caddy which is a powerful platform. Caddy manages the requests from the outside world and forward requests via an internal docker network to the vaultwarden server.
Caddy must accept only HTTPS connections and use the standard directory for certificates /opt/vaultwarden/certs. Therefore we create this directory.
I use Letsencrypt certificates which are managed automatically by certbot and stored in the directory /etc/letsencrypt on my host server. The keys are set up the following structure:
- In /etc/letsencrypt/live/<domain> there are only symlinks that point
- to the real files in /etc/letsencrypt/archive/<domain>.
Copy the fullchain.pem and privkey.pem file from /etc/letsencrypt/live/<domain> to /opt/vaultwarden/certs and set the permissions accordingly.
#create certs directory
mkdir /opt/vaultwarden/vw-data
#change into certs directory
cd /opt/vaultwarden/certs
#copy the files
cp /etc/letsencrypt/live/<domain>/fullchain.pem fullchain.pem
cp /etc/letsencrypt/live/<domain>/privkey.pem privkey.pem
#set owner and group vaultwarden
chown vaultwarden:vaultwarden fullchain.pem
chown vaultwarden:vaultwarden privkey.pem
#set access (read write only vaultwarden)
chmod 600 privkey.pem
chmod 600 fullchain.pem
note: The cp command will place the actual files (not the symlinks) in /opt/vaultwarden/certs unless you specify a -P or -d option.
Run Vaultwarden and Caddy as docker containers
The following setup creates a secure, email-enabled Vaultwarden instance behind a Caddy reverse proxy with HTTPS and admin access, running entirely via Docker.
Create docker-compose.yml
This docker-compose.yml file sets up two services Vaultwarden and Caddy to host a self-hosted password manager with HTTPS support.
#docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: always
environment:
DOMAIN: "<yourDomain>"
SIGNUPS_ALLOWED: "false"
SMTP_HOST: "<yourSmtpServer>"
SMTP_FROM: "<yourEmail>"
SMTP_FROM_NAME: "<yourName>"
SMTP_USERNAME: "<yourEmail>"
SMTP_PASSWORD: "<yourSmtpPasswd>"
SMTP_SECURITY: "force_tls"
SMTP_PORT: "465"
ADMIN_TOKEN: '<yourAdminToken>'
volumes:
- ./vw-data:/data
caddy:
image: caddy:2
container_name: caddy
restart: always
ports:
- 443:443
- 443:443/udp
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-config:/config
- ./caddy-data:/data
- ./certs:/certs:ro
environment:
DOMAIN: "<yourDomain>"
LOG_FILE: "/data/access.log"
vaultwarden service:
Runs the Vaultwarden server (a lightweight Bitwarden-compatible backend).
- Disables user signups (SIGNUPS_ALLOWED: „false“).
- Configures SMTP settings for sending emails (e.g. for password resets).
- Sets a secure admin token (using Argon2 hash) to access the /admin interface.
- Persists Vaultwarden data to ./vw-data on the host.
Enable admin page access:
With the ADMIN_TOKEN set we enable login to the admin site via <yourDomain>/admin. First create a secure admin password. The <yourAdminToken> value can be created with your admin password piped into argon2. The result is a hash value that must be a little modified and then inserted as <yourAdminToken>. It is important that you use single quotes in docker-compose.yml when you insert <yourAdminToken>.
sudo apt install -y argon2
echo -n '<yourAdminPassword>' | argon2 somesalt -e
#This is the result of the argon2 hashing
$argon2i$v=19$m=4096,t=3,p=1$c29tZXNhbHQ$D...
Then you modify the hash by adding a $ sign in front of each $ sign in the hash. In this case we add 5 $ signs.
#original value
$argon2i$v=19$m=4096,t=3,p=1$c29thbHQ$D...
#modified value
$$argon2i$$v=19$$m=4096,t=3,p=1$$c29thbHQ$$D...
Then you put the modified value in single quotes into docker-compose.yml.
#docker-compose.yml
.....
ADMIN_TOKEN: '$$argon2i$$v=19$$m=4096,t=3,p=1$$c29thbHQ$$D...'
Now you can access the admin page with <yourDomain>/admin and login with your admin password.
caddy service:
Uses Caddy web server to reverse proxy to Vaultwarden.
- Handles HTTPS using custom certificates from ./certs.
- Binds to port 443 for secure access.
- Reads its configuration from ./Caddyfile.
- Logs access to /data/access.log (mapped from ./caddy-data on the host).
Create Caddyfile
Caddy is a modern, powerful web server that automatically handles HTTPS, reverse proxying, and more. Caddy acts as a secure HTTPS reverse proxy, forwarding external requests to the Vaultwarden Docker container running on internal port 80.
This Caddyfile defines how Caddy should serve and protect your vaultwarden instance over HTTPS.
#Caddyfile
https://<domain> {
log {
level INFO
output file /data/access.log {
roll_size 10MB
roll_keep 10
}
}
# Use custom certificate and key
tls /certs/fullchain.pem /certs/privkey.pem
# This setting may have compatibility issues with some browsers
# (e.g., attachment downloading on Firefox). Try disabling this
# if you encounter issues.
encode zstd gzip
# Admin path matcher
@adminPath path /admin*
# Basic Auth for admin access
handle @adminPath {
# If admin path require basic auth
basicauth {
superadmin <passwdhash>
}
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
# Everything else
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
Domain
https://<domain>
This defines the domain name Caddy listens on (e.g. https://yourinstance.example.com).
Logging
log {
level INFO
output file /data/access.log {
roll_size 10MB
roll_keep 10
}
}
Logs all access to a file inside the container (/data/access.log), with log rotation.
TLS with Custom Certificates
tls /certs/fullchain.pem /certs/privkey.pem
Use your own Let’s Encrypt certificates from mounted files rather than auto-generating them.
Compression
encode zstd gzip
Enables modern compression methods to improve performance, though may cause issues with attachments on some browsers.
Admin Area Protection
@adminPath path /admin*
Matches all requests to /admin paths.
handle @adminPath {
basicauth {
superadmin <passwdhash>
}
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
- Requires HTTP Basic Auth for access to /admin.
- Proxies/Forwards authenticated admin requests to the Vaultwarden container.
- Ensures the backend sees the original client IP address.
All Other Requests
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
- Proxies/Forwards all non-/admin traffic directly to Vaultwarden container.
- Ensures the backend sees the original client IP address.
Protect your admin page
To protect your admin page we can use a HTTP Basic Auth. This mean whenever you access <yourDomain>/admin a login window will pop up in your browser and ask you to provide a user name and a password. We use htpasswd which is part of the apache2-utils.
#install apache2-utils if not available
sudo apt install apache2-utils
#Create a hash for the user admin (you can use any user-name)
htpasswd -nB admin
New password:
Re-type new password:
admin:$2y$05$HZukVJWhWMrT7qMO2n65bm/5JYlt5tO...
Option | Description |
---|---|
-n
| Displays the result only on the console instead of writing it to a file. |
-B
| Uses the bcrypt hash algorithm, which is supported by Caddy and is very secure. |
admin
| The username for basic auth access (for example admin). |
Then you insert only the hash (without admin: ….) into the Caddyfile.
#Caddyfile
.....
handle @adminPath
bash
basicauth {
admin $2y$05$HZukVJWhWMrT7qMO2n65bm/5JYlt5tO...
}
Create sync-certs.sh script and root crontab
This script copies renewed Let’s Encrypt certificates from the standard location to a custom destination /opt/vaultwarden/certs, sets strict permissions, assigns correct ownership, and restarts the Caddy container to apply the new certificates. At the end it reloads the Caddy container (via docker-compose restart) to apply new certs.
#!/bin/bash
# Variables
DOMAIN="<Domain>"
SRC="/etc/letsencrypt/live/$DOMAIN"
DEST="/opt/vaultwarden/certs"
# Check Source Directory
if [ ! -d "$SRC" ]; then
echo "Certificate Path $SRC not found"
exit 1
fi
# Check Destination Directory
if [ ! -d "$DEST" ]; then
mkdir -p "$DEST"
chown vaultwarden:vaultwarden "$DEST"
chmod 700 "$DEST"
echo "Target Path $DEST created"
fi
# Copy files (overwrite)
cp "$SRC/fullchain.pem" "$DEST/fullchain.pem"
cp "$SRC/privkey.pem" "$DEST/privkey.pem"
# set owner:group vaultwarden
chown vaultwarden:vaultwarden "$DEST/fullchain.pem"
chown vaultwarden:vaultwarden "$DEST/privkey.pem"
# set access (read write only vaultwarden)
chmod 600 "$DEST/privkey.pem"
chmod 600 "$DEST/fullchain.pem"
echo "[sync-certs] Certificates for $DOMAIN synced"
# successful sync of certificates – caddy re-start
echo "[sync-certs] re-start caddy ..."
cd /opt/vaultwarden
/usr/local/bin/docker-compose restart caddy
echo "[sync-certs] caddy reloaded new certificates"
Check Source Directory
if [ ! -d "$SRC" ]; then
echo "Certificate Path $SRC not found"
exit 1
fi
- What it checks: Whether the Let’s Encrypt certificate source directory for the domain exists.
- Why it’s needed:
- Let’s Encrypt stores certificates as symlinks in /etc/letsencrypt/live/<domain>.
- If this folder doesn’t exist, the script stops immediately to avoid copying from a missing or invalid source.
- Fail-safe: Prevents copying non-existent files, which would cause later commands to fail.
Check and Create Destination Directory
if [ ! -d "$DEST" ]; then
mkdir -p "$DEST"
chown vaultwarden:vaultwarden "$DEST"
chmod 700 "$DEST"
echo "Target Path $DEST created"
fi
- What it checks: Whether the destination directory for the copied certificates exists.
- If not:
- It creates the directory (mkdir -p ensures parent paths are created if missing).
- Sets secure permissions:
- Owner: vaultwarden
- Permissions: 700 – only the vaultwarden user can access the directory.
- Why this matters:
- The vaultwarden container or process needs access to the certificates.
- These permissions ensure only vaultwarden can read the certs, improving security.
Start, Stop and Check containers
Here are the most important docker-compose commands. Run these commands when you are in the directory where the docker-compose.yml file is and be sure that the logged in user is in the docker group (otherwise these commands work with sudo).
Command | Description |
---|---|
docker-compose up -d | Start all services defined in docker-compose.yml in detached mode (background). |
docker-compose down | Stop and remove all services and associated networks/volumes (defined in the file). |
docker-compose restart | Restart all services. |
docker-compose stop | Stop all running services (without removing them). |
docker-compose start | Start services that were previously stopped. |
docker-compose logs | Show logs from all services. |
docker-compose logs -f | Tail (follow) logs in real time. |
Here art the most important docker commands to check the status of containers.
Command | Description |
---|---|
docker ps | List running containers. |
docker ps -a | List all containers (running + stopped). |
docker logs <container-name> | Show logs of a specific container. |
docker logs -f <container-name> | Tail logs in real time. |
docker inspect <container-name> | Show detailed info about a container. |
docker top <container-name> | Show running processes inside the container. |
docker exec -it <container-name> /bin/sh | Start a shell session in the container. |
docker stats | Live resource usage (CPU, RAM, etc.) of containers. |