> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosted Bitwarden password manager server

> Self-hosted password manager with Docker Compose and Cloudflare Tunnel: zero open ports, hidden host IP; choose Vaultwarden, Bitwarden Lite, or Standard.

export const OptionPicker = ({options = [], lang = "zh", label}) => {
  const t = lang === "en" ? {
    pick: label || "Pick an option to compare",
    rec: "Recommended"
  } : {
    pick: label || "點選項目比較",
    rec: "推薦"
  };
  const opts = Array.isArray(options) ? options : [];
  const [sel, setSel] = useState(0);
  if (opts.length === 0) return null;
  const idx = Math.min(sel, opts.length - 1);
  const cur = opts[idx];
  const css = `
  .op-root{--op-bg:#FAF8F3;--op-surface:rgba(0,0,0,0.025);--op-border:rgba(0,0,0,0.09);--op-text:#2b2722;--op-dim:#6f6a62;--op-faint:#8a8378;--op-accent:#bf7551;border:1px solid var(--op-border);border-radius:14px;background:var(--op-bg);color:var(--op-text);overflow:hidden;}
  .dark .op-root{--op-bg:#1b1a18;--op-surface:rgba(255,255,255,0.03);--op-border:rgba(255,255,255,0.08);--op-text:#e7e3da;--op-dim:#a8a299;--op-faint:#8a8378;--op-accent:#cf8a68;}
  .op-head{padding:12px 16px 11px;border-bottom:1px solid var(--op-border);font-size:12.5px;color:var(--op-dim);display:flex;align-items:center;gap:8px;}
  .op-head-ic{color:var(--op-accent);flex-shrink:0;}
  .op-tabs{display:flex;gap:8px;padding:14px 16px 4px;flex-wrap:wrap;}
  .op-tab{position:relative;flex:1 1 130px;min-width:120px;text-align:left;background:transparent;border:1px solid var(--op-border);border-radius:11px;padding:11px 13px;cursor:pointer;color:inherit;font:inherit;transition:border-color .15s,background .15s,box-shadow .15s;}
  .op-tab:hover{background:var(--op-surface);}
  .op-tab-on{border-color:rgba(191,117,81,.5);background:rgba(191,117,81,.07);box-shadow:0 0 0 3px rgba(191,117,81,.1);}
  .op-tab-name{font-size:14.5px;font-weight:600;line-height:1.3;}
  .op-tab-on .op-tab-name{color:var(--op-accent);}
  .op-badge{display:inline-block;margin-top:6px;font-size:10.5px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:2px 7px;border-radius:20px;background:rgba(191,117,81,.14);color:var(--op-accent);}
  .op-rec{position:absolute;top:10px;right:11px;font-size:10px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;color:var(--op-accent);}
  .op-body{padding:6px 16px 16px;}
  .op-specs{border:1px solid var(--op-border);border-radius:10px;overflow:hidden;margin-bottom:11px;}
  .op-row{display:flex;border-top:1px solid var(--op-border);font-size:13px;}
  .op-row:first-child{border-top:none;}
  .op-k{width:42%;flex-shrink:0;padding:8px 12px;color:var(--op-dim);background:var(--op-surface);}
  .op-v{flex:1 1 0;min-width:0;padding:8px 12px;font-weight:500;}
  .op-note{font-size:13px;line-height:1.6;color:var(--op-dim);padding:0 2px;}
  `;
  return <div className="op-root">
      <style>{css}</style>
      <div className="op-head"><svg className="op-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z" /><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65" /><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65" /></svg>{t.pick}</div>
      <div className="op-tabs">
        {opts.map((o, i) => <button key={i} type="button" className={"op-tab" + (i === idx ? " op-tab-on" : "")} onClick={() => setSel(i)}>
            {o.recommend && <span className="op-rec">★ {t.rec}</span>}
            <div className="op-tab-name">{o.name}</div>
            {o.badge && <span className="op-badge">{o.badge}</span>}
          </button>)}
      </div>
      <div className="op-body">
        <div className="op-specs">
          {(cur.specs || []).map((row, i) => <div className="op-row" key={i}>
              <div className="op-k">{row[0]}</div>
              <div className="op-v">{row[1]}</div>
            </div>)}
        </div>
        {cur.note && <div className="op-note">{cur.note}</div>}
      </div>
    </div>;
};

export const ArchFlow = ({nodes = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    flow: "Data flow",
    hint: "Click any stage to see what it does",
    stage: "Stage"
  } : {
    flow: "資料流",
    hint: "點任一階段看它做什麼",
    stage: "階段"
  };
  const safe = Array.isArray(nodes) ? nodes : [];
  const [active, setActive] = useState(0);
  if (safe.length === 0) return null;
  const idx = Math.min(active, safe.length - 1);
  const current = safe[idx];
  const css = `
  .af-root{--af-bg:#FAF8F3;--af-surface:rgba(191,117,81,0.06);--af-surface2:rgba(0,0,0,0.025);--af-border:rgba(0,0,0,0.09);--af-text:#2b2722;--af-dim:#6f6a62;--af-faint:#8a8378;--af-rail:rgba(0,0,0,0.12);--af-accent:#bf7551;border:1px solid var(--af-border);border-radius:14px;background:var(--af-bg);color:var(--af-text);overflow:hidden;}
  .dark .af-root{--af-bg:#1b1a18;--af-surface:rgba(207,138,104,0.09);--af-surface2:rgba(255,255,255,0.03);--af-border:rgba(255,255,255,0.08);--af-text:#e7e3da;--af-dim:#a8a299;--af-faint:#8a8378;--af-rail:rgba(255,255,255,0.13);--af-accent:#cf8a68;}
  .af-head{padding:13px 18px 11px;display:flex;align-items:center;gap:9px;flex-wrap:wrap;border-bottom:1px solid var(--af-border);}
  .af-head-ic{color:var(--af-accent);flex-shrink:0;}
  .af-head-t{font-size:12px;font-weight:700;letter-spacing:.6px;color:var(--af-dim);text-transform:uppercase;}
  .af-head-h{font-size:12px;color:var(--af-faint);}
  .af-grid{display:flex;gap:0;align-items:stretch;}
  .af-list{flex:1 1 0;min-width:0;padding:12px 12px 16px;}
  .af-node{display:flex;align-items:stretch;gap:10px;width:100%;text-align:left;background:transparent;border:none;cursor:pointer;padding:0;color:inherit;font:inherit;-webkit-tap-highlight-color:transparent;}
  .af-railcol{width:22px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;}
  .af-dot{width:9px;height:9px;border-radius:50%;margin-top:14px;background:var(--af-faint);opacity:.5;flex-shrink:0;transition:background .15s,box-shadow .15s,opacity .15s;}
  .af-line{width:2px;flex:1 1 0;background:var(--af-rail);min-height:10px;}
  .af-body{flex:1 1 0;min-width:0;padding:9px 12px;margin:2px 0;border-radius:9px;border:1px solid transparent;transition:background .15s,border-color .15s;}
  .af-node:hover .af-body{background:var(--af-surface2);}
  .af-node-on .af-body{background:var(--af-surface);border-color:rgba(191,117,81,.30);}
  .af-node-on .af-dot{background:var(--af-accent);opacity:1;box-shadow:0 0 0 4px rgba(191,117,81,.15);}
  .af-label{font-size:14.5px;font-weight:550;line-height:1.45;}
  .af-node-on .af-label{color:var(--af-accent);}
  .af-detail{width:300px;flex-shrink:0;border-left:1px solid var(--af-border);padding:16px 18px;background:var(--af-surface2);}
  .af-detail-k{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--af-accent);margin-bottom:7px;}
  .af-detail-l{font-size:16px;font-weight:600;margin-bottom:9px;line-height:1.35;}
  .af-detail-d{font-size:13.5px;line-height:1.65;color:var(--af-dim);}
  @media (max-width:640px){.af-grid{flex-direction:column;}.af-detail{width:auto;border-left:none;border-top:1px solid var(--af-border);}}
  `;
  return <div className="af-root">
      <style>{css}</style>
      <div className="af-head">
        <svg className="af-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="8" height="8" x="3" y="3" rx="2" /><path d="M7 11v4a2 2 0 0 0 2 2h4" /><rect width="8" height="8" x="13" y="13" rx="2" /></svg>
        <span className="af-head-t">{t.flow}</span>
        <span className="af-head-h">{t.hint}</span>
      </div>
      <div className="af-grid">
        <div className="af-list">
          {safe.map((n, i) => <button key={i} type="button" className={"af-node" + (i === idx ? " af-node-on" : "")} onClick={() => setActive(i)} onMouseEnter={() => setActive(i)}>
              <div className="af-railcol">
                <div className="af-dot" />
                {i < safe.length - 1 && <div className="af-line" />}
              </div>
              <div className="af-body">
                <div className="af-label">{n.label}</div>
              </div>
            </button>)}
        </div>
        <div className="af-detail">
          <div className="af-detail-k">{t.stage} {idx + 1} / {safe.length}</div>
          <div className="af-detail-l">{current.label}</div>
          <div className="af-detail-d">{current.detail}</div>
        </div>
      </div>
    </div>;
};

`Bitwarden` · `Vaultwarden` · `Docker` · `Cloudflare Tunnel` · `Self-Hosted` · `Security`

## Overview

A self-hosted Bitwarden password manager server deployed through Docker Compose and Cloudflare Tunnel. The host opens no inbound ports and the host IP is not exposed. All encryption and decryption happens on the client side; the server stores only ciphertext (zero-knowledge architecture), so the vault security is identical across all three deployment options.

Vaultwarden (community edition) is the default recommendation: it is compatible with all official Bitwarden clients, has the lowest resource footprint, and all features are free. The official Lite and Standard editions are available for situations that require official vendor support or organizational compliance guarantees.

## Core features

* Cloudflare Tunnel reverse tunnel: no inbound ports, host IP not exposed
* Cloudflare Edge handles TLS termination, WAF, and DDoS protection; no self-managed certificates needed
* Three configurations covering personal, small-team, and organizational needs, each with a separate Docker Compose file and environment variable template
* Compatible with all official Bitwarden clients (desktop, browser extension, mobile)
* Vaultwarden Admin Panel included, with optional Cloudflare Access restriction
* Client-side encryption: zero-knowledge architecture, the server never sees plaintext passwords

## Architecture

<ArchFlow
  lang="en"
  nodes={[
{ label: "Clients", detail: "Web, desktop, and mobile clients use the official Bitwarden apps. All encryption and decryption happen on the client; the server stores only ciphertext (zero-knowledge architecture)." },
{ label: "Cloudflare Edge", detail: "Handles TLS termination, WAF, and DDoS protection. No self-managed certificates are needed and the host IP is never exposed." },
{ label: "Cloudflare Tunnel", detail: "Encrypted reverse tunnel. The cloudflared container dials out to Cloudflare, so the host opens no inbound ports." },
{ label: "cloudflared container", detail: "Tunnel connector that receives traffic from Cloudflare Edge and forwards it to the Bitwarden container on the same Docker network." },
{ label: "Bitwarden/Vaultwarden container", detail: "Runs Vaultwarden (1 container), official Lite (1 container), or official Standard (10 microservice containers) depending on the chosen configuration." },
{ label: "bw-data/ volume", detail: "Persistent storage for the vault database and attachments, mounted into the container so data survives container rebuilds." }
]}
/>

<Tree>
  <Tree.Folder name="bitwarden-server" defaultOpen>
    <Tree.File name="docker-compose.yml · Vaultwarden (default) + Cloudflared" />

    <Tree.File name=".env.template · environment variable template" />

    <Tree.Folder name="lite · official Lite edition" defaultOpen>
      <Tree.File name="docker-compose.yml" />

      <Tree.File name=".env.template" />
    </Tree.Folder>

    <Tree.Folder name="standard · official Standard edition (microservices, 10 containers)" defaultOpen>
      <Tree.File name="docker-compose.yml" />

      <Tree.File name=".env.template" />
    </Tree.Folder>
  </Tree.Folder>
</Tree>

## Configuration options

<OptionPicker
  lang="en"
  options={[
{
name: "Vaultwarden", badge: "Default", recommend: true,
specs: [["Image", "vaultwarden/server"], ["Maintained by", "Community (dani-garcia)"], ["Containers", "1"], ["Min memory", "~150 MB"], ["Database", "SQLite / MySQL / PG"], ["Free features", "All features free"], ["Audience", "Personal, home"]],
note: "Lowest footprint, all features free, compatible with every official client. The default choice for most personal and home setups."
},
{
name: "Bitwarden Lite",
specs: [["Image", "ghcr.io/bitwarden/lite"], ["Maintained by", "Bitwarden Inc."], ["Containers", "1"], ["Min memory", "~200 MB"], ["Database", "SQLite / MySQL / PG"], ["Free features", "Some paid features"], ["Audience", "Small team"]],
note: "Official single-container build for small teams that want vendor backing without the full microservice stack."
},
{
name: "Bitwarden Standard",
specs: [["Image", "ghcr.io/bitwarden/self-host/*"], ["Maintained by", "Bitwarden Inc."], ["Containers", "10 (microservices)"], ["Min memory", "~4 GB"], ["Database", "SQL Server 2022"], ["Free features", "Some paid features"], ["Audience", "Organization, compliance"]],
note: "Full official microservice architecture with higher resource needs (4 GB+ RAM), for organizations with compliance or official-support requirements."
}
]}
/>

## Quick start

<Steps>
  <Step title="Configure the Cloudflare Tunnel">
    Create a tunnel in the Zero Trust dashboard and copy the Tunnel Token. Set the Public Hostname service URL to the container name (e.g. `http://bitwarden:80`), not localhost.
  </Step>

  <Step title="Set environment variables">
    ```bash theme={null}
    cp .env.template .env
    ```

    Fill in the Cloudflare Tunnel Token, the optional Vaultwarden Admin Token, and any other required values.
  </Step>

  <Step title="Start the service">
    ```bash theme={null}
    docker compose up -d
    ```

    For Lite or Standard, `cd` into the `lite/` or `standard/` directory first, then run the same command.
  </Step>

  <Step title="Configure the client">
    Open an official Bitwarden client, enter your self-hosted server's Cloudflare URL in the server settings, then create an account and sign in.
  </Step>
</Steps>

## Notes

<Warning>
  The Admin Panel (`/admin`) should be restricted with a Cloudflare Access Policy or disabled in production if not needed. `.env` contains database passwords and the Admin Token; never commit it to version control. The official Standard edition requires a Bitwarden Installation ID obtained from bitwarden.com/host before deployment.
</Warning>

## In practice

Suited for individuals or small teams who want full control over their vault data without relying on a third-party cloud service. The Cloudflare Tunnel makes an instance running on a NAS or home server securely reachable from anywhere, without firewall rules or a static IP.

## Links

* GitHub: [felimet/bitwarden-server](https://github.com/felimet/bitwarden-server)
* Vaultwarden project: [github.com/dani-garcia/vaultwarden](https://github.com/dani-garcia/vaultwarden)
* Cloudflare Tunnel docs: [developers.cloudflare.com/cloudflare-one/connections/connect-networks](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
* Bitwarden self-host docs: [bitwarden.com/help/self-host-an-organization](https://bitwarden.com/help/self-host-an-organization/)
