# How to self-host Vaultwarden with Docker Compose and HTTPS

> Run Vaultwarden, a Bitwarden-compatible password server, with Docker Compose behind Caddy, then close sign-ups, hash the admin token and set up backups.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

Vaultwarden is an alternative server for the Bitwarden password manager, written in Rust. It implements the Bitwarden client API, runs as one small container, and works with the regular Bitwarden browser extensions, desktop apps and mobile apps. That makes it a common choice for individuals, families and small teams who want to keep their password vault on a server they control. Vaultwarden is an unofficial project and is not associated with Bitwarden, Inc.

This guide runs the official `vaultwarden/server` image with Docker Compose, publishes it only on `127.0.0.1`, and puts Caddy in front of it for HTTPS, which the web vault requires. You then create your account, close open registration, protect the admin page with an Argon2 hashed token, and set up backups and restores that follow the project's wiki. Banning repeated failed logins with fail2ban is covered as an optional step.

## Prerequisites

- A server running **Ubuntu 24.04 LTS**, **Ubuntu 26.04 LTS**, **Debian 12** or **Debian 13**. The steps work on a HyperDC Linux VPS, VDS or dedicated server with root access.
- A non-root user with `sudo` rights and SSH key login. See [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- Docker Engine with the Compose plugin: [Install Docker on Ubuntu](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian).
- A domain name such as `vault.example.com` with an A record (and an AAAA record if you use IPv6) pointing at your server.
- Caddy installed on the host as described in [Caddy as a reverse proxy](/guides/caddy-reverse-proxy).

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU |
| RAM | Not published | 1 GB |
| Disk | Not published | 10 GB plus room for attachments and backups |

The project does not publish minimum requirements. The right-hand column is a conservative starting point for a personal or small-team vault, not a benchmark.

## Step 1 — Create the project folder

Keep everything for this app in one folder that your admin user owns:

```bash
sudo mkdir -p /opt/vaultwarden
sudo chown $USER:$USER /opt/vaultwarden
cd /opt/vaultwarden
```

The vault data will live in `/opt/vaultwarden/vw-data`, which Docker creates on first start.

## Step 2 — Generate a hashed admin token

Vaultwarden has an admin page at `/admin` where you can invite users, review accounts and change settings. Its password is the `ADMIN_TOKEN` value. The wiki recommends storing it as an Argon2id PHC string instead of plain text, so that someone who reads your configuration does not learn the password.

First create a long random password and save it in a password manager outside this vault (you may need it when the vault is unavailable):

```bash
openssl rand -base64 32
```

Then run Vaultwarden's built-in `hash` command in a temporary container. It asks for the password twice and prints the hash only if both entries match:

```bash
docker run --rm -it vaultwarden/server:latest /vaultwarden hash
```

The output ends with a line that starts with `ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$`. Copy the value between the single quotes. The default parameters match Bitwarden's own settings; `--preset owasp` selects the lighter OWASP minimum instead.

## Step 3 — Write the environment and Compose files

Store the hash in an `.env` file next to the Compose file. Following the wiki, wrap the value in **single quotes** and keep every `$` single; Compose then reads it literally:

```bash
nano .env
```

```env
VAULTWARDEN_ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$REPLACE_WITH_YOUR_HASH'
```

```bash
chmod 600 .env
```

Now create `compose.yaml`. It is based on the project's own Compose example, with your domain filled in and sign-ups still allowed so you can create the first account:

```yaml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
      ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN}
    volumes:
      - ./vw-data/:/data/
    ports:
      - 127.0.0.1:8000:80
```

- `DOMAIN` must be the full HTTPS address users open; some features break without it.
- `127.0.0.1:8000:80` keeps the container off the public network. Only Caddy on the same server can reach it.
- The `latest` tag follows the newest stable release, which the wiki recommends for most users. Version tags such as `1.37.4` and Alpine variants (`latest-alpine`) also exist.

Check the file for errors before starting:

```bash
docker compose config --quiet
```

No output means the file is valid. A warning such as `The "argon2id" variable is not set` means Compose tried to expand the `$` signs; check the single quotes in `.env`.

## Step 4 — Start Vaultwarden

```bash
docker compose up -d
docker compose ps
docker compose logs --tail 20
curl -I http://127.0.0.1:8000
```

`docker compose ps` shows the container as running (the image has a health check, so it turns `healthy` after a short while), and `curl` returns `HTTP/1.1 200 OK`. Confirm that the admin token arrived exactly as intended, with single `$` signs and no extra quotes:

```bash
docker compose exec vaultwarden printenv ADMIN_TOKEN
```

## Step 5 — Serve Vaultwarden over HTTPS with Caddy

Add a site block to `/etc/caddy/Caddyfile`. The `header_up` line passes the visitor's real IP address to Vaultwarden, which reads the `X-Real-IP` header by default; logs, rate limiting and fail2ban depend on it:

```caddyfile
vault.example.com {
    reverse_proxy 127.0.0.1:8000 {
        header_up X-Real-IP {remote_host}
    }
}
```

Reload Caddy and make sure the firewall allows only SSH and web traffic:

```bash
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
curl -I https://vault.example.com
```

The last command returns `HTTP/2 200` with a valid certificate. Caddy also passes the WebSocket connection that Vaultwarden uses for live sync notifications, with no extra configuration. If you prefer another proxy, see [Nginx with Certbot](/guides/nginx-reverse-proxy-certbot) or [Traefik](/guides/traefik-reverse-proxy).

## Step 6 — Create your account and close registration

Open `https://vault.example.com` in your browser, choose **Create account**, and set a strong master password. Neither you nor the admin page can recover a forgotten master password, so store a recovery copy safely.

As soon as your account (and any family or team accounts) exist, stop anyone else from registering. Change the line in `compose.yaml`:

```yaml
      SIGNUPS_ALLOWED: "false"
```

Then recreate the container:

```bash
docker compose up -d
```

The **Create account** link disappears from the login page. To add people later, open the admin page, go to **Users** and use **Invite User**. Without SMTP no mail is sent; the invited person registers at `https://vault.example.com/#/signup` with exactly the invited email address. Organization owners can also invite members, because `INVITATIONS_ALLOWED` defaults to true.

> **Tip**
>
> To avoid even a short window with open registration, start with `SIGNUPS_ALLOWED: "false"` and invite your own address from the admin page instead.

To send invitations, email verification and password hints by email, add `SMTP_HOST: "smtp.example.com"`, `SMTP_PORT: "587"`, `SMTP_SECURITY: "starttls"`, `SMTP_FROM`, `SMTP_USERNAME` and `SMTP_PASSWORD` to the `environment` section of `compose.yaml` (keep the password in `.env`, like the admin token) and recreate the container with `docker compose up -d`.

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

## Step 7 — Use the admin page and turn on two-step login

Open `https://vault.example.com/admin` and log in with the password you hashed in Step 2 (not the hash). Admin sessions last 20 minutes by default.

> **Warning**
>
> The first time you click **Save** on the admin page, Vaultwarden writes `vw-data/config.json`. Values in that file take precedence over the environment variables in `compose.yaml`. After that, change those settings on the admin page, or edit `config.json` while the container is stopped.

Then protect your own account: in the web vault open **Settings**, then **Security**, then **Two-step login**, and set up an authenticator app. Save the recovery code somewhere outside the vault.

To connect the Bitwarden apps and browser extensions, choose the self-hosted option on the login screen and enter `https://vault.example.com` as the server URL.

If you rarely need the admin page, you can disable it: remove the `ADMIN_TOKEN` line (and any `admin_token` entry in `config.json`) and recreate the container.

## Step 8 — Ban repeated failed logins with fail2ban (optional)

Vaultwarden logs failed logins with the client IP address. fail2ban can read that log and block an address after a few failures. First let Vaultwarden write a log file inside its data folder by adding one variable to the `environment` section of `compose.yaml`:

```yaml
      LOG_FILE: "/data/vaultwarden.log"
```

```bash
docker compose up -d
sudo apt install fail2ban
```

Create the filter `/etc/fail2ban/filter.d/vaultwarden.local` with the pattern from the wiki:

```ini
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =
```

Create the jail `/etc/fail2ban/jail.d/vaultwarden.local`:

```ini
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /opt/vaultwarden/vw-data/vaultwarden.log
backend = auto
maxretry = 3
bantime = 14400
findtime = 14400
```

```bash
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden
```

Because Caddy runs on the host, the default ban actions block the visitor before Caddy sees the request. If your reverse proxy runs inside Docker instead, the wiki explains the `chain = FORWARD` change, and it also has a second filter for failed admin page logins. Vaultwarden does not rotate this log file itself; add a `logrotate` rule with `copytruncate` if it grows large.

## Back up and restore

All state lives in `/opt/vaultwarden/vw-data`. The wiki lists what matters:

- `db.sqlite3`: the database with almost all vault data (required),
- `attachments/`: file attachments (required),
- `config.json`: settings saved on the admin page, in plain text (recommended),
- `rsa_key*`: the keys that sign login tokens (recommended),
- `sends/`: Send attachments, which are meant to be temporary (optional),
- `icon_cache/`: cached website icons (not needed).

Do not copy a live `db.sqlite3` file on its own. Vaultwarden has a built-in command (since 1.32.1) that writes a consistent copy of the SQLite database next to the original; the Docker image has no `sqlite3` binary, so use this command:

```bash
cd /opt/vaultwarden
docker compose exec vaultwarden /vaultwarden backup
```

It prints a line like `Backup to '/data/db_20261009_031500.sqlite3' was successful`; inside the container `/data` is your `vw-data` folder. Now archive that copy together with the other files and your Compose setup, leaving out the live database files and the icon cache, then delete the temporary copy:

```bash
sudo mkdir -p /opt/backups
sudo tar --exclude='db.sqlite3*' --exclude='icon_cache' -czf /opt/backups/vaultwarden-$(date +%F).tar.gz -C /opt/vaultwarden compose.yaml .env vw-data
sudo rm /opt/vaultwarden/vw-data/db_*.sqlite3
```

The archive contains secrets (the admin token hash, `config.json`, the RSA keys and your encrypted vault). Restrict its permissions, run the backup daily with cron or a systemd timer, and copy it off the server, for example to [your own S3 storage](/guides/self-hosted-s3-storage) on another machine.

To restore, stop Vaultwarden, unpack the archive, and turn the dated database copy back into `db.sqlite3`. The wiki says to delete any existing `db.sqlite3-wal` first, otherwise the restored database can be corrupted:

```bash
cd /opt/vaultwarden
docker compose down
sudo tar -xzf /opt/backups/vaultwarden-2026-10-09.tar.gz -C /opt/vaultwarden
sudo rm -f vw-data/db.sqlite3 vw-data/db.sqlite3-wal vw-data/db.sqlite3-shm
sudo mv vw-data/db_*.sqlite3 vw-data/db.sqlite3
docker compose up -d
```

The same steps restore onto a new server after you install Docker and Caddy. Test a restore now and then, and keep the original data until you have confirmed the restored vault works.

## Update Vaultwarden

The Bitwarden apps update themselves, so the wiki stresses keeping the server on the latest release. Read the [release notes](https://github.com/dani-garcia/vaultwarden/releases) first, because some releases remove old options or change proxy behaviour, take a backup, then pull and recreate:

```bash
cd /opt/vaultwarden
docker compose pull
docker compose up -d
docker compose logs --tail 20
docker image prune
```

The web vault is bundled with the server image, so it always matches the server version.

## Troubleshooting

### The web vault shows an error or stays blank over HTTP

The web vault needs a secure context for the Web Crypto API. Open it through `https://vault.example.com`, not through `http://` or the server's IP address, and make sure `DOMAIN` in `compose.yaml` is the same HTTPS address.

### You are using a plain text ADMIN_TOKEN which is insecure

Either `.env` still holds a plain password, or a value saved on the admin page overrides it. Check both places, as the wiki suggests: `docker compose exec vaultwarden printenv ADMIN_TOKEN` and `sudo grep admin_token vw-data/config.json`. The output must be the `$argon2id$` string with single `$` signs and no extra quotes.

### WARNING: The argon2id variable is not set

Compose tried to expand the `$` signs in the hash. Put the value in `.env` inside single quotes, as in Step 3, or double every `$` if you write the hash directly into `compose.yaml`.

### The admin page says it is disabled

`ADMIN_TOKEN` is empty inside the container. Run `docker compose config` to see whether the variable from `.env` reached the service, then recreate the container with `docker compose up -d`.

### Logs and fail2ban show 127.0.0.1 instead of the visitor's IP

The `header_up X-Real-IP {remote_host}` line is missing from the Caddy site block, or Caddy was not reloaded. If you put Cloudflare's proxy in front, the wiki explains which header to use instead.

## Next steps

- Keep off-site copies of your backups on [self-hosted S3 storage](/guides/self-hosted-s3-storage) running on a second server.
- Make the admin page reachable only through a VPN with [a WireGuard VPN server](/guides/wireguard-vpn-server).
- Learn more about the Compose file format in [Docker Compose basics](/guides/docker-compose-basics).
- Compare servers for a private password vault on the [Vaultwarden hosting](/vaultwarden-hosting) page.
- Read the [Vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki) for SMTP, push notifications and other options.

## Frequently asked questions

### Is Vaultwarden the official Bitwarden server?

No. Vaultwarden is an independent, unofficial server written in Rust that implements the Bitwarden client API. The project states that it is not associated with Bitwarden or Bitwarden, Inc. The regular Bitwarden apps and browser extensions can connect to it.

### Why does the web vault not work over plain HTTP?

The web vault relies on the browser Web Crypto API, which only runs in a secure context. Serve Vaultwarden over HTTPS, for example with Caddy as shown in this guide, and set DOMAIN to the https address.

### Do I need an SMTP server for Vaultwarden?

Not for a personal vault. Email is needed for invitation mails, email verification, password hint mails and email-based two-step login. You can add SMTP_HOST, SMTP_FROM and the related variables later.

### Can I use MySQL or PostgreSQL instead of SQLite?

Yes. Vaultwarden supports MySQL/MariaDB and PostgreSQL through DATABASE_URL. The default SQLite database is enough for individuals and small teams, and the backup steps in this guide are written for SQLite.

### How big a server does Vaultwarden need?

The project does not publish minimum requirements. It is a single lightweight process, so a small Linux VPS with 1 vCPU and 1 GB of RAM is a conservative starting point for a personal, family or small-team vault.

---

Source: <https://hyperdc.com/guides/tutorials/install-vaultwarden>\
Updated: 2026-10-09
