セルフホストで管理すると安心なものの一つにパスワード管理がありますね。外部サービスを利用せず、それでいてどこからでも利用出来るのは重宝します。
使いやすいパスワード管理ソフトとしては「Vaultwarden」があります。
利用するには、HTTPSアクセスが必須となります。
NginxとCaddyの比較
これには、以前も軽く紹介したNginxを利用する方法もありますが、個人用途で、特にTailscale内で利用するならCaddyが適していますね。

Caddyの動作イメージ
Caddyの動作イメージは次のようになります。

Caddyを導入しワイルドカード証明書を使うようにすれば、Caddyだけが外部(Tailscale)に面して、各サービスはホストのポートを公開しないこともでき、Dockerのコンテナ名がそのままホスト名になるため、IPアドレス管理が不要になる点もメリットですね(ワイルドカード証明書を使うにはDNS-01チャレンジが必要で、そのためにはドメインのDNSを外部プロバイダ(Cloudflare、Route53など)で管理している必要があります。Tailscaleのプライベートドメイン(.ts.net)はTailscale社が管理しているため、外部からDNS-01チャレンジができません)
実際の導入方法は後述するので、まずは構成だけ。
ディレクトリ構成
/opt/
├── caddy/
│ ├── docker-compose.yml
│ └── Caddyfile
└── vaultwarden/
├── docker-compose.yml
└── data/
Caddy側 docker-compose.yml
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
networks:
caddy_net:
name: caddy_net # 他のcomposeから参照するための固定名
driver: bridge
# Caddyコンテナをそのネットワークにアタッチ
# (上のservices.caddyにも追記)
services.caddy に追加:
networks:
- caddy_net
Vaultwarden側 docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
volumes:
- ./data:/data
environment:
- WEBSOCKET_ENABLED=true
- SIGNUPS_ALLOWED=false
# portsは書かない → ホストには公開しない
networks:
- caddy_net
networks:
caddy_net:
external: true # Caddyが作ったネットワークを借りる
```
---
## `Caddyfile`
```
vaultwarden.your-machine.tail????.ts.net {
reverse_proxy vaultwarden:80
}
コンテナ名 vaultwarden がそのままDNS名として使えます。
新サービスを追加するときのルール
サービスのcomposeに以下を追加するだけです:
networks:
caddy_net:
external: true
そしてCaddyfileに1ブロック追記。ネットワーク設定の変更は不要で、スケールアウトが非常に楽な構成です。
Caddyを導入する
まずはTailscaleのサイトでHTTPSを有効化します。Tailscale管理コンソール → DNS → Enable HTTPS をオン。

Tailscaleで HTTPS Certificatesを有効にすると、*.tail1234.ts.net のワイルドカード証明書がTailscaleの CAから自動発行されますが、CaddyがLet’s Encryptと同じ仕組み (ACME) で自動取得・自動更新してくれます。
Caddyの導入も下記を貼り付けるだけでOKです。
#!/bin/bash
# ============================================================
# Caddy セットアップスクリプト
# インストール先: /opt/docker/caddy
# 対応OS: Ubuntu 24.x / 25.x
# ============================================================
set -euo pipefail
CADDY_DIR="/opt/docker/caddy"
echo "======================================"
echo " Caddy セットアップ"
echo "======================================"
# ── 前提確認 ────────────────────────────────────────
if ! command -v docker &>/dev/null; then
echo "[ERROR] Docker が見つかりません。先に Docker をインストールしてください。"
exit 1
fi
if ! docker compose version &>/dev/null; then
echo "[ERROR] Docker Compose が見つかりません。"
exit 1
fi
# ── Tailscale FQDN の取得 ────────────────────────────
echo ""
echo "[1/4] Tailscale FQDN を取得中..."
TS_FQDN=""
if command -v tailscale &>/dev/null; then
TS_FQDN=$(tailscale status --json 2>/dev/null \
| python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
name = d['Self']['DNSName'].rstrip('.')
print(name)
except Exception:
pass
" 2>/dev/null || true)
fi
if [ -z "$TS_FQDN" ]; then
echo ""
echo " [WARN] Tailscale FQDN を自動取得できませんでした。"
echo " Tailscale が起動しているか確認: sudo tailscale status"
echo ""
read -rp " FQDN を手動入力 (例: myhost.tail1234.ts.net): " TS_FQDN
if [ -z "$TS_FQDN" ]; then
echo "[ERROR] FQDN が入力されませんでした。終了します。"
exit 1
fi
fi
echo " ✅ Tailscale FQDN: ${TS_FQDN}"
# ── ディレクトリ作成 ─────────────────────────────────
echo ""
echo "[2/4] ディレクトリを作成中..."
mkdir -p "${CADDY_DIR}/config"
mkdir -p "${CADDY_DIR}/logs"
echo " ✅ ${CADDY_DIR}/config … Caddyfile 置き場"
echo " ✅ ${CADDY_DIR}/logs … アクセスログ"
# ── Caddyfile 生成 ───────────────────────────────────
echo ""
echo "[3/4] 設定ファイルを生成中..."
# caddy fmt に通るよう、インデントはタブではなく4スペースで統一
cat > "${CADDY_DIR}/config/Caddyfile" << CADDYFILE
# ============================================================
# Caddyfile — Tailscale FQDN: ${TS_FQDN}
#
# サービス追加後のリロード(ダウンタイムなし):
# docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# ============================================================
{
email admin@${TS_FQDN}
admin localhost:2019
}
# ── サービス追加テンプレート ──────────────────────────────
#
# 【別コンテナ(caddy-net に参加させること)】
# myapp.${TS_FQDN} {
# reverse_proxy myapp-container:3000
# }
#
# 【ホスト上のプロセス】
# myapp.${TS_FQDN} {
# reverse_proxy localhost:8080
# }
#
# 【Basic 認証付き】
# private.${TS_FQDN} {
# basicauth {
# # ハッシュ生成: docker exec -it caddy caddy hash-password
# admin \$2a\$14\$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# }
# reverse_proxy localhost:9000
# }
# ──────────────────────────────────────────────────────────
# デフォルト: 動作確認用(サービス追加後はこのブロックを削除または上書き)
${TS_FQDN} {
respond "Caddy is running on ${TS_FQDN}" 200
}
CADDYFILE
# ── docker-compose.yml 生成 ──────────────────────────
# ヒアドキュメント内の $ をエスケープして変数展開を防ぐ
cat > "${CADDY_DIR}/docker-compose.yml" << 'COMPOSE_EOF'
# ============================================================
# Caddy 単体構成
# ============================================================
networks:
caddy-net:
name: caddy-net
driver: bridge
volumes:
caddy_data:
caddy_config:
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- /opt/docker/caddy/config:/etc/caddy
- caddy_data:/data
- caddy_config:/config
- /opt/docker/caddy/logs:/var/log/caddy
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
networks:
- caddy-net
healthcheck:
test: ["CMD", "caddy", "version"]
interval: 30s
timeout: 5s
retries: 3
COMPOSE_EOF
echo " ✅ ${CADDY_DIR}/config/Caddyfile"
echo " ✅ ${CADDY_DIR}/docker-compose.yml"
# ── 起動 ─────────────────────────────────────────────
echo ""
echo "[4/4] コンテナを起動中..."
docker compose -f "${CADDY_DIR}/docker-compose.yml" up -d
# ── Caddyfile フォーマット警告を解消 ─────────────────
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
# ── 完了 ─────────────────────────────────────────────
echo ""
echo "======================================"
echo " ✅ セットアップ完了!"
echo "======================================"
echo ""
echo " Tailscale FQDN : ${TS_FQDN}"
echo " 確認URL : https://${TS_FQDN}"
echo ""
echo " ── よく使うコマンド ──────────────────────────"
echo " # Caddyfile 編集後リロード"
echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
echo ""
echo " # ログ確認"
echo " docker compose -f ${CADDY_DIR}/docker-compose.yml logs -f"
echo ""
echo " # Basic 認証パスワードハッシュ生成"
echo " docker exec -it caddy caddy hash-password"
echo "=============================================="
このスクリプトがやること
tailscale status --json から Self.DNSName(例: myhost.tail1234.ts.net)を取得して、CaddyfileとDocker Composeに自動で埋め込みます。Tailscaleが起動していない場合は手動入力にフォールバックします。
サービスを追加するときの流れ
1. Caddyfileを編集
sudo nano /opt/docker/caddy/config/Caddyfile
2. ダウンタイムなしでリロード
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
これだけです。再起動不要で即反映されます。
他のコンテナをCaddyの後ろに置く場合
そのサービスの docker-compose.yml に以下を追記するだけで、コンテナ名でアクセスできるようになります。
networks:
caddy-net:
external: true
services:
myapp:
...
networks:
- caddy-net
Caddyfile側は reverse_proxy myapp:3000 のようにコンテナ名で指定できます。
パスワード管理のVaultwardenを導入
続いてパスワード管理のVaultwardenを導入します。
#!/bin/bash
# ============================================================
# Vaultwarden セットアップスクリプト
# インストール先: /opt/docker/vaultwarden
# 前提: Caddy(setup-caddy.sh)導入済み
# 使い方: sudo bash setup-vaultwarden.sh
# ============================================================
set -euo pipefail
VW_DIR="/opt/docker/vaultwarden"
CADDY_CONF="/opt/docker/caddy/config/Caddyfile"
echo "======================================"
echo " Vaultwarden セットアップ"
echo "======================================"
# ── 前提確認 ────────────────────────────────────────
if ! command -v docker &>/dev/null; then
echo "[ERROR] Docker が見つかりません。"
exit 1
fi
if [ ! -f "$CADDY_CONF" ]; then
echo "[ERROR] Caddyfile が見つかりません: ${CADDY_CONF}"
echo " 先に setup-caddy.sh を実行してください。"
exit 1
fi
if ! docker inspect caddy &>/dev/null; then
echo "[ERROR] Caddy コンテナが起動していません。"
echo " 先に setup-caddy.sh を実行してください。"
exit 1
fi
# ── Tailscale FQDN の取得 ────────────────────────────
echo ""
echo "[1/4] Tailscale FQDN を取得中..."
TS_FQDN=""
if command -v tailscale &>/dev/null; then
TS_FQDN=$(tailscale status --json 2>/dev/null \
| python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d['Self']['DNSName'].rstrip('.'))
except Exception:
pass
" 2>/dev/null || true)
fi
if [ -z "$TS_FQDN" ]; then
echo ""
echo " [WARN] Tailscale FQDN を自動取得できませんでした。"
read -rp " FQDN を手動入力 (例: myhost.tail1234.ts.net): " TS_FQDN
if [ -z "$TS_FQDN" ]; then
echo "[ERROR] FQDN が入力されませんでした。終了します。"
exit 1
fi
fi
echo " ✅ Tailscale FQDN: ${TS_FQDN}"
# ── ディレクトリ・トークン準備 ───────────────────────
echo ""
echo "[2/4] ディレクトリとAdmin Tokenを準備中..."
mkdir -p "${VW_DIR}/data"
# Admin Token 生成(既存があれば引き継ぐ)
if [ -f "${VW_DIR}/.env" ] && grep -q "^ADMIN_TOKEN=" "${VW_DIR}/.env"; then
ADMIN_TOKEN=$(grep "^ADMIN_TOKEN=" "${VW_DIR}/.env" | cut -d= -f2)
echo " ✅ 既存の Admin Token を引き継ぎます。"
else
ADMIN_TOKEN=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 64)
install -m 600 /dev/null "${VW_DIR}/.env"
echo "ADMIN_TOKEN=${ADMIN_TOKEN}" > "${VW_DIR}/.env"
echo " ✅ Admin Token を生成しました。"
fi
# ── docker-compose.yml 生成 ──────────────────────────
echo ""
echo "[3/4] 設定ファイルを生成中..."
# ヒアドキュメント内で変数展開が必要な箇所のみ展開
cat > "${VW_DIR}/docker-compose.yml" << COMPOSE
networks:
caddy-net:
external: true
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
volumes:
- ./data:/data
environment:
DOMAIN: "https://${TS_FQDN}"
WEBSOCKET_ENABLED: "true"
SIGNUPS_ALLOWED: "true"
ADMIN_TOKEN: "${ADMIN_TOKEN}"
LOG_LEVEL: "warn"
networks:
- caddy-net
COMPOSE
echo " ✅ ${VW_DIR}/docker-compose.yml"
# ── Caddyfile にVaultwardenブロックを追加 ────────────
# すでにvaultwardenブロックがあれば既存のFQDNブロックを置き換え
# なければ末尾に追記
NEW_BLOCK="${TS_FQDN} {
reverse_proxy /notifications/hub vaultwarden:3012
reverse_proxy vaultwarden:80
}"
if grep -q "reverse_proxy.*vaultwarden" "$CADDY_CONF"; then
echo " ✅ Caddyfile に既存のVaultwardenブロックがあります。スキップ。"
else
# FQDNのブロックが存在する場合(デフォルトのrespondブロック)は置き換え
if grep -q "^${TS_FQDN}" "$CADDY_CONF"; then
python3 - "$CADDY_CONF" "$TS_FQDN" "$NEW_BLOCK" << 'PYEOF'
import sys, re
conf_path, fqdn, new_block = sys.argv[1], sys.argv[2], sys.argv[3]
with open(conf_path) as f:
content = f.read()
# FQDNブロック(ネストした {} を正しく対応させて検索)
def find_block(text, fqdn):
pattern = re.compile(r'^' + re.escape(fqdn) + r'\s*\{', re.MULTILINE)
m = pattern.search(text)
if not m:
return None, None
start = m.start()
depth, i = 0, m.end() - 1
while i < len(text):
if text[i] == '{':
depth += 1
elif text[i] == '}':
depth -= 1
if depth == 0:
return start, i + 1
i += 1
return None, None
start, end = find_block(content, fqdn)
if start is not None:
new_content = content[:start] + new_block + '\n' + content[end:].lstrip('\n')
with open(conf_path, 'w') as f:
f.write(new_content)
print(f" ✅ {fqdn} ブロックをVaultwardenに置き換えました。")
else:
print(f" [WARN] {fqdn} ブロックが見つかりませんでした。末尾に追記します。")
with open(conf_path, 'a') as f:
f.write(f'\n{new_block}\n')
PYEOF
else
# FQDNブロック自体がない場合は末尾に追記
printf '\n%s\n' "$NEW_BLOCK" >> "$CADDY_CONF"
echo " ✅ Caddyfile 末尾にVaultwardenブロックを追記しました。"
fi
fi
# Caddyfileフォーマット整形(警告解消)
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
echo " ✅ Caddyfile をフォーマットしました。"
# ── 起動 ─────────────────────────────────────────────
echo ""
echo "[4/4] Vaultwarden を起動中..."
docker compose -f "${VW_DIR}/docker-compose.yml" up -d
# caddy-net に参加できているか確認
NETWORKS=$(docker inspect vaultwarden --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null || true)
if echo "$NETWORKS" | grep -q "caddy-net"; then
echo " ✅ caddy-net に参加済み"
else
echo " [ERROR] caddy-net への参加に失敗しました。"
echo " docker inspect vaultwarden で確認してください。"
exit 1
fi
# Caddy リロード
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
echo " ✅ Caddy をリロードしました。"
# ── 完了 ─────────────────────────────────────────────
echo ""
echo "======================================"
echo " ✅ セットアップ完了!"
echo "======================================"
echo ""
echo " URL : https://${TS_FQDN}"
echo " Admin: https://${TS_FQDN}/admin"
echo " Token: ${ADMIN_TOKEN}"
echo ""
echo " ⚠ Admin Token は以下に保存されています:"
echo " ${VW_DIR}/.env"
echo ""
echo " ── よく使うコマンド ──────────────────────────"
echo " # ログ確認"
echo " docker compose -f ${VW_DIR}/docker-compose.yml logs -f"
echo ""
echo " # 停止"
echo " docker compose -f ${VW_DIR}/docker-compose.yml down"
echo ""
echo " # 更新"
echo " docker compose -f ${VW_DIR}/docker-compose.yml pull"
echo " docker compose -f ${VW_DIR}/docker-compose.yml up -d"
echo "=============================================="





