VPSでVaultwardenを構築する方法|Dockerでのセルフホスト手順
VPS上でDockerとVaultwardenを使い、Bitwarden互換のパスワードマネージャーを構築する手順を解説します。HTTPS設定の重要性や、Admin APIの有効化、Fail2banによる保護、バックアップのテスト方法など、運用に欠かせない重要ポイントを網羅しています。
作成するもの
完全に自身で管理できるパスワードマネージャーです。HTTPSを終端する reverse proxy の背後で、1つの小さな container として Vaultwarden を実行します。スマートフォン、ノートPC、ブラウザの official な Bitwarden app から、このサーバーに接続します。Vaultwarden は Bitwarden server API を Rust で再実装しており、bitwarden.com と同じプロトコルを使用します。そのため、すべての official client がそのまま動作します。また、公式の multi-container stack とは異なり、メモリ消費量は約 100 MB に収まります。
インストール自体は、Compose を数行記述するだけです。実際に重要となる、かつ失敗の原因となる要素は以下の3点です。まず、web vault を読み込む前に TLS が設定されている必要があります。次に、自身の account が作成された直後に、public signups を無効にする必要があります。最後に、すべてのパスワードが格納されているため、data volume の backup と test-restore が不可欠です。
前提条件と注意点
- Docker Engine と Compose plugin がインストールされた VPS。Ubuntu 24.04 KVM で、root または sudo 権限が必要です。512 MB の RAM で十分動作しますが、1 GB あれば余裕があります。これは非常に軽量なサービスであり、セルフホストすべきサービスのリスト の上位に含まれます。
- VPS の
vault.example.comを指す A レコード(IPv6 を使用する場合は AAAA レコードも)を持つドメイン。TLS 証明書はこのドメイン名に対して発行されるため、作業前に DNS の解決ができることを確認してください。 - インターネットに対してポート 80 と 443 が開放されていること。これらはリバースプロキシで終端させてください。Vaultwarden で直接終端しないでください。ポート 80 は、ACME 証明書チャレンジと HTTP から HTTPS へのリダイレクトにのみ使用されます。
- 最大の注意点:Bitwarden クライアントは HTTPS ではないサーバーとの通信を拒否します。「まず HTTP でテストする」ことはできません。理由は次のセクションで説明します。
なぜ公式のBitwardenスタックではなくVaultwardenなのか
クライアントは同じですが、リソース消費は大幅に抑えられます。公式のセルフホスト版Bitwardenは、複数のコンテナ(MSSQL, Nginx, Identity, Api, Adminなど)のセットとして提供され、約2 GBのRAMを必要とします。一方、Vaultwardenは単一のバイナリであり、デフォルトではすべてをSQLiteデータベースに保存するため、アイドル時のメモリ使用量は数十MB程度です。個人、家族、または小規模なチームにとって、Vaultwardenは最適な選択肢です。また、Bitwarden APIを忠実に実装しているため、データはVaultwardenとbitwarden.comの間で移行可能です。
一方で、エンタープライズ向けの機能の多くは利用できません。SCIMプロビジョニングは利用できず(ただし、1.35.0で試験的なOpenID Connect SSOが導入されました)、運用者は自身で管理する必要があります。つまり、パッチ適用、HTTPSの設定、およびバックアップは運用者の責任となります。本ガイドでは、これら3つの作業について解説します。
HTTPS が必須である理由
Bitwarden の Web vault およびブラウザ拡張機能は、Web Crypto API (window.crypto.subtle) を使用してブラウザ内で暗号化キーを生成します。ブラウザは crypto.subtle を secure context(HTTPS または http://localhost の特殊なケース)でのみ公開します。通常の http://vault.example.com では undefined となるため、アプリがキーを生成しようとした瞬間にエラーが発生し、コンソールに以下が表示されます。
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')ページがフリーズするか、一般的な crypto エラーが表示され、ログインできません。Desktop、Mobile、およびブラウザの各クライアントは、セルフホストされた URL に対して独自のチェックを実行します。http(または到達不能な)エンドポイントに対しては、以下のエラーを返して接続を拒否します。
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.両者の原因は同じです。有効な HTTPS がありません。そのため、まず TLS を構築してください。中身を素早く確認する場合であっても、http で vault を開かないでください。
Step 1 — DNSとリバースプロキシ (TLSを優先)
レコードをVPSに向けて、正しいアドレスに解決されることを確認してください。
dig +short vault.example.com出力される行がVPSのIPアドレスである必要があります。空白または誤っている場合は、DNSを修正してTTLの経過を待ってください。名前解決できないドメインに対しては、証明書の発行に失敗します。
HTTPSフロントエンドにはTraefikを使用します。TraefikはLet's Encrypt証明書を自動的に発行・更新し、Composeに直接組み込めます。まだ使用していない場合は、まず Traefikリバースプロキシと自動TLSの設定 を行ってください。これにより、外部Dockerネットワーク (以下 proxy) と、Vaultwardenサービスが接続するACMEリゾルバ (letsencrypt) が作成されます。手動で発行した証明書を使用する通常のnginxでも、Vaultwarden側からは同様に動作します。
Traefikの代わりにnginxとCertbotを使用したい場合: Vaultwardenを 127.0.0.1:8080 に配置してください (サービスに ports: ["127.0.0.1:8080:80"] を追加し、Traefikのlabelsを削除します)。その後、証明書を発行してプロキシしてください。証明書に関する詳細は Certbotとnginxを使用したLet's Encrypt証明書の発行 で説明しています。重要な追加設定は、通知パスにおけるWebSocketのアップグレードです。
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}X-Real-IP の行に注意してください。これがあることで、後ほどFail2banが 127.0.0.1 ではなく、実際の攻撃者を検知できるようになります。Traefikを使用するかnginxを使用するかに関わらず、このガイドの他の手順はすべて共通です。
Step 2 — the Compose file
Create the project directory first. This guide uses /opt/vaultwarden, which makes the Compose project name — and therefore the data volume, vaultwarden_vw-data — predictable; the Fail2ban and backup steps below depend on that exact name.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCreate a .env for the admin secret and the Compose file in that directory.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGenerate that token with openssl rand -base64 48 and paste it in. (A stronger hashed form is covered next; a long random string is fine to start.)
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: trueTwo things about this file carry the whole design. There is no ports: mapping, so Vaultwarden is reachable only through Traefik and its TLS — publishing its port on the host is how people serve the vault over http by accident. And DOMAIN must be the full public HTTPS URL: it is baked into attachment links, WebAuthn 2FA and the notifications endpoint, so a wrong or http value breaks those even when the site loads. The latest tag is a deliberate exception to the usual never-latest rule — Vaultwarden ships its stable releases as a single rolling image, with :testing as the separate pre-release channel — so update on purpose and skim the release notes before you pull.
Bring it up and watch the log:
docker compose up -d
docker compose logs -f vaultwardenA correct start ends with a line like Rocket has launched from http://0.0.0.0:80. Give Traefik a few seconds to fetch the certificate, then load https://vault.example.com — you should get the Bitwarden web vault with a valid padlock and no certificate warning.
Step 3 — 強固な ADMIN_TOKEN と $$ の罠
ADMIN_TOKEN は /admin を保護します。このパネルはインスタンス上のすべてのユーザーと設定を読み取れるため、root パスワードと同様に扱ってください。2つの形式があります。
シンプルな形式は、すでに openssl rand -base64 48 で生成したランダムな文字列です。base64 には $ が含まれないため、エスケープ処理なしでそのまま .env に使用できます。
より強固な形式は Argon2 PHC ハッシュです。これを使用すると、平文のトークンがディスクに保存されません。同じイメージを使用して生成してください:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspプロンプトが2回表示され、$argon2id$v=19$... で始まる文字列が出力されます。ここで、多くのユーザーが1時間ほど費やすことになる罠があります。Docker Compose は $ を変数展開として扱うため、Compose ファイルにハッシュを貼り付ける際は、すべての $ を $$ に書き換える必要があります。environment: の直下に記述し、.env を介さず、引用符(quotes)で囲まないでください:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObG$ をそのままにすると、Compose は The "argon2id" variable is not set という警告を出し、トークンを空にします。その結果、/admin が正しいパスワードを拒否します。docker compose up -d を実行し、プロンプトで入力した平文は自身のパスワード管理ツールに保存しておいてください。
Step 4 — アカウントの登録とアクセス制限
SIGNUPS_ALLOWED: "true"を使用してhttps://vault.example.comを開きます。Create accountをクリックし、メールアドレスと強力な master password を入力して登録してください。この master password は再取得できません。リセット機能もありません。必ず安全な場所に保管してください。
次に、アクセス制限を行います。Compose file を編集し、signups を無効にします。
SIGNUPS_ALLOWED: "false"docker compose up -d で設定を再適用してください。これは必須のセキュリティ設定であり、後回しにはできません。設定を有効にしない場合、URLを見つけた第三者(およびクローラー)がサーバーにアカウントを作成できてしまいます。ユーザーは あなたの vault を閲覧することはできませんが、リソースを消費し、プライベートなインスタンスが公開サービスになってしまいます。設定が有効になっていない兆候は、/admin に身に覚えのないアカウントがリストされていることです。
後から家族やチームメンバーを追加する場合は、公開 signups を有効にする必要はありません。/admin の Invite User ボタンを使用してください。この機能を利用するには、招待者がリンクを受け取れるよう SMTP の設定が必要です。
Step 5 — /admin へのアクセス
https://vault.example.com/admin にアクセスし、プレーンテキストの admin token を入力してください(ハッシュ値ではなく、ランダムな文字列またはハッシュ化前のパスワードを入力します)。この画面では、ユーザー一覧の表示、設定の調整、テストメールの送信、データベースのスナップショット作成が可能です。
ページが 404 Not Found を返す場合、ADMIN_TOKEN が空または未設定の状態です。この場合、パネル機能は完全に無効化されます。パネルを使用する必要がない場合は、この設定で問題ありません。ページは読み込まれるが token が拒否される場合は、以下の失敗リストにある $$ のエスケープに関する注意点を確認してください。token を忘れた場合は、復元プロンプトはありません。.env または Compose file を編集して新しい token を設定し、docker compose up -d を実行してください。
Step 6 — Bitwarden clients の接続
すべての公式クライアントはセルフホストサーバーを指すことができます。通常のストアから Bitwarden の desktop、mobile、または browser クライアントをインストールしてください。特別な Vaultwarden 用ビルドは 不要 です。
ログインする前に、ログイン画面にある設定アイコン(Self-hosted または Region → Self-hosted と表示)を開きます。Server URL に https://vault.example.com を入力して保存してください。その後、登録した email と master password でログインします。クライアントは即座に接続され、認証情報の入力と保存の提案が表示されます。
クライアントに This is not a recognized Bitwarden server. You may need to check with your provider or update your server. と表示される場合は、URL が間違っているか、http を使用しているか、または証明書が信頼されていません。まず https://vault.example.com がブラウザで正常に読み込めるか再確認してください。他のデバイスでの更新の遅延は WebSocket push が原因であり、詳細は後述します。
Step 7 — ログインエンドポイント用のFail2ban jail
Vaultwardenは、LOG_FILEで設定されたファイルにすべてのログイン失敗を記録します。これはブルートフォース攻撃の防御に最適です。Fail2banをまだ導入していない場合は、Fail2ban SSH hardening guideを参照してください。ここでは、Vault用のjailを1つ追加します。
まず、Fail2banがログを読み取れるよう、ホスト上のnamed volumeの場所を特定します。
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'実行結果には/var/lib/docker/volumes/vaultwarden_vw-data/_dataのような内容が表示されます。ログは、その中のvaultwarden.logにあります。filterを作成します。
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =次に、jailを作成します。
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600sudo systemctl restart fail2banでリロードし、sudo fail2ban-client status vaultwardenで確認します。
以下の3つのDockerに関する詳細設定によって、保護の成否が決まります。
- ログに
IP: 127.0.0.1やプロキシのIPアドレスが記録されている場合、Vaultwardenはプロキシ自体をBANしてしまいます。この場合は、IP_HEADERをプロキシが実際に送信するヘッダーに設定してください(Traefikの場合はX-Forwarded-For、上記のnginxブロックの場合はX-Real-IP、Cloudflare経由の場合はCF-Connecting-IP)。
- 使用するiptables chainはプロキシに依存します。ポートを公開したコンテナとしてTraefikを実行している場合、トラフィックはDockerの
FORWARDを経由するため、上記のようにDOCKER-USERにBANを設定する必要があります。一方、Step 1でホスト上のnginxを選択した場合は、接続はホストのINPUTchainで終了するため、DOCKER-USERのBANでは検知できません。その場合はchain = DOCKER-USERの行を削除し、Fail2banにデフォルトのINPUTchainを使用させてください。
- デフォルトのポートベースではなく
banaction = iptables-allportsを使用してください。このjailにはポート定義がありません。DOCKER-USERで全ポートを対象にBANすることで、攻撃者をホスト上のすべての公開サービスから確実に遮断できます。
Step 8 — vaultのバックアップを取得し、実際にリストアする
vw-data volumeがパスワードマネージャーの実体です。ここにはdb.sqlite3(全エントリ)、attachments/およびsends/ディレクトリ、ログインセッションを署名するrsa_key.*ファイル、そして管理パネルのconfig.jsonが含まれます。これらの一部でも欠けたバックアップは、いざという時に機能しません。
Vaultwardenが書き込み中のdb.sqlite3をコピーすると、ファイルが破損する可能性があります。そのため、コールドスナップショットを取得してください。停止時間は数秒です。
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwardennightlyでcronを実行し、.tgzをサーバーの外へコピーしてください。保護対象のサーバー内にのみ存在するバックアップは、バックアップとは呼べません。推奨される方法は 別のサーバーやobject storageへのnightly restic backup です。これを使用すると、アーカイブの暗号化とスナップショットの重複排除が自動で行われます。管理パネルの Backup Database ボタンは、SQLiteファイルのみを素早くスナップショットとして保存できますが、添付ファイルとkeyは含まれません。
次に、真のバックアップであるかを検証するための手順を行います。一度リストアを行い、動作を確認してください。
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/server自身のlaptopからssh -L 8888:127.0.0.1:8888 you@your-vpsでトンネルを張り、http://localhost:8888を開きます。localhostはセキュアなコンテキストであるため、crypto.subtleが利用可能です。この環境では、許可された唯一の場所として、plain http経由でvaultの復号が行われます。マスターパスワードでログインし、エントリが存在することを確認してください。エントリが確認できれば、database、RSA keys、およびマスターパスワードのすべてが正常に復元されています。これにより、新しいVPSへ数分で再構築できます。Ctrl-Cでcontainerを停止し、/tmp/vw-restoreを削除してください。
発生するエラーと表示される文字列
ブラウザのコンソールに Cannot read properties of undefined (reading 'importKey') が表示される。 Vaultがhttp経由で読み込まれているため、crypto.subtle が undefined になっています。https:// 経由でのみアクセス可能です。プロキシで HTTP から HTTPS へのリダイレクトを設定してください。
クライアントに This is not a recognized Bitwarden server... が表示される。 Server URL が http であるか、入力ミス、あるいは証明書が信頼されていません。https://vault.example.com で南京錠のアイコンが表示されていることを確認し、クライアントの self-hosted 設定に再入力してください。
/admin が正しいパスワードを拒否する。 Argon2 ハッシュのエスケープが失われています。Compose 内のすべての $ は $$ である必要があります。または、プレーンテキストではなくハッシュそのものを入力しています。
デバイス間同期が遅い。コンソールに WebSocket connection to 'wss://vault.example.com/notifications/hub' failed が表示される。 プロキシが Upgrade/Connection ヘッダーを転送していません。Traefik は自動で行いますが、nginx の場合は Step 1 の upgrade 行が2つ必要です。Vault 自体は動作していますが、起動時に同期が行われます。v1.31.0 以降、旧来の専用ポート 3012 は廃止されたため、個別の WebSocket ルートは不要です。
Fail2ban が ban を報告しているが、攻撃者が接続を継続している。 IP_HEADER が誤っているため 127.0.0.1 が ban されています。あるいは、ban が誤った iptables chain に適用されています。chain = DOCKER-USER と banaction = iptables-allports を設定してください。
Upgrades
新しい image を pull して再作成してください。named volume とデータは保持されます。
docker compose pull
docker compose up -dVaultwarden は頻繁にリリースされます。リリース内容に migration の指示が含まれる場合があるため、patch version を固定するのではなく、プロジェクトのリリースノートを確認してください。メジャーアップデートの前には必ずバックアップを取得してください。tarball を新しい volume にリストアすることで、ロールバックが可能です。
FAQ
VaultwardenはBitwardenと同じものですか?
Vaultwardenは、公式ではなく、互換性のある独立したサーバーです。VaultwardenはBitwardenのサーバーAPIをRustで再実装しています。そのため、公式のデスクトップ、モバイル、ブラウザ、およびCLIクライアントがすべて動作します。また、公式のスタックよりも大幅に少ないリソースで動作します。Vaultの形式は同じであるため、エクスポートとインポートによって双方向の移行が可能です。
HTTPSは必須ですか?LAN内でhttpを使用して実行することは可能ですか?
localhostのテスト用途を除き、HTTPSが必要です。BitwardenのWeb Vaultおよび拡張機能は、ブラウザのWeb Crypto APIを使用します。これはセキュアなコンテキストでのみ利用可能です。そのため、通常のhttp経由ではクライアントがCannot read properties of undefinedを返し、ログインできません。動作する唯一のhttpアドレスはhttp://localhostです。ステップ8のリストアテストでSSHトンネルを使用するのはこのためです。
見知らぬユーザーがサーバーに登録するのを防ぐにはどうすればよいですか?
自分のアカウントを作成した直後に、ComposeファイルでSIGNUPS_ALLOWED: "false"を設定し、docker compose up -dを実行してください。その後は、/adminのInvite Userボタンから新しいユーザーを追加してください。招待リンクを送信するために、SMTPの設定が必要です。予期しないアカウントが作成されていないか、定期的に管理者ユーザーリストを確認してください。
VaultwardenのVaultをバックアップする方法は?
コンテナを一時停止し、vw-dataボリューム全体(db.sqlite3、attachments/、sends/、config.json、およびrsa_key.*ファイル)をアーカイブしてください。その後、アーカイブをサーバー外にコピーします。夜間のcronでの実行を推奨します。サーバー稼働中にSQLiteファイルをコピーすると、スナップショットが破損する恐れがあるため、停止した状態で取得してください。最も重要なことは、バックアップが有効であることを確認するために、一度使い捨てのコンテナにリストアしてログインを試すことです。
パスワードのセルフホストは本当に安全ですか?
はい。本ガイドで説明する3つの事項(適切なHTTPS、サインアップの制限と強力なadmin token、およびテスト済みのバックアップ)を遵守していれば安全です。Vaultはマスターパスワードによってクライアントサイドで暗号化されます。そのため、サーバー側でもパスワードが平文で露出することはありません。db.sqlite3が盗まれても、マスターパスワードがなければ役に立ちません。ただし、パッチ適用とバックアップは自己責任となります。そのため、Fail2banの導入とリストアの習慣化は必須事項です。