Skip to Content

Production Guide: Deploy Vaultwarden with Docker Compose + NGINX + PostgreSQL on Ubuntu

A production-first blueprint for running a secure self-hosted password manager with TLS, backups, and operational guardrails.

Most teams discover too late that password managers become production infrastructure the moment they store root credentials, API tokens, and cloud console access. A lightweight setup may work in a pilot, but once operations, engineering, and support depend on it daily, reliability and security requirements rise quickly. This guide shows how to deploy Vaultwarden on Ubuntu using Docker Compose, NGINX, and PostgreSQL in a way that is practical for real operations: TLS termination, least-privilege secrets handling, health validation, and recoverable backups.

The approach is opinionated for production. We isolate services on an internal Docker network, expose only NGINX on public ports, avoid embedding secrets in repository files, and provide repeatable checks for every stage. If you are replacing ad-hoc credential sharing and need a centralized, auditable password vault with stable operational behavior, this runbook gives you a clean path from host preparation to day-2 operations.

Architecture and flow overview

The stack includes three components. Vaultwarden provides the application API and web UI. PostgreSQL stores encrypted metadata and operational records with stronger consistency and backup tooling than SQLite for multi-user production usage. NGINX acts as the edge reverse proxy, handling TLS and forwarding application traffic to Vaultwarden on the private network. Inbound traffic reaches NGINX over HTTPS, NGINX forwards to Vaultwarden, and Vaultwarden reads and writes state in PostgreSQL.

Operationally, this architecture keeps external exposure simple while giving you mature database controls. It also makes lifecycle work easier: you can patch NGINX, rotate certificates, or tune PostgreSQL without tearing down the whole stack. The same pattern scales cleanly from a small internal team to larger multi-department use.

Prerequisites

  • Ubuntu 22.04 or 24.04 host with sudo access
  • A DNS record such as vault.example.com pointing to your server
  • Docker Engine and Docker Compose plugin installed
  • At least 2 vCPU, 4 GB RAM, and 30 GB available disk for logs, backups, and updates
  • A plan for off-host backup storage (object storage or secured secondary node)

Step-by-step deployment

Step 1: Prepare host baseline and firewall

sudo apt update && sudo apt -y upgrade
sudo apt install -y ufw curl jq ca-certificates gnupg
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo timedatectl set-timezone UTC

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

These baseline controls reduce accidental exposure and keep host behavior predictable. Avoid opening the Vaultwarden container port publicly; only NGINX should be reachable from the internet.

Step 2: Create directory layout and least-privilege ownership

sudo mkdir -p /opt/vaultwarden/{{compose,nginx,backups,pgdata,data}}
sudo useradd --system --home /opt/vaultwarden --shell /usr/sbin/nologin vwsvc || true
sudo chown -R $USER:$USER /opt/vaultwarden
chmod 750 /opt/vaultwarden

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Using dedicated paths keeps future migration and restore operations straightforward. Restrictive permissions limit who can read service files containing environment hints.

Step 3: Define Compose stack for Vaultwarden + PostgreSQL

services:
  postgres:
    image: postgres:16
    container_name: vw-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: vaultwarden
      POSTGRES_USER: vaultwarden
      POSTGRES_PASSWORD: ${PG_PASSWORD}
    volumes:
      - /opt/vaultwarden/pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vaultwarden -d vaultwarden"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [vw_internal]

  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://vaultwarden:${PG_PASSWORD}@postgres:5432/vaultwarden
      DOMAIN: https://${VW_DOMAIN}
      SIGNUPS_ALLOWED: "false"
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      WEBSOCKET_ENABLED: "true"
      ROCKET_PORT: "8080"
      LOG_LEVEL: warn
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - /opt/vaultwarden/data:/data
    networks: [vw_internal]

networks:
  vw_internal:
    name: vw_internal

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Pinning a major PostgreSQL version and enabling health checks improves upgrade safety. The internal network boundary prevents direct internet access to application and database containers.

Step 4: Configure environment variables and secrets

cd /opt/vaultwarden/compose
PG_PASSWORD=$(openssl rand -base64 48 | tr -d '
')
ADMIN_TOKEN=$(openssl rand -base64 48 | tr -d '
')
cat > .env <

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Store .env outside source control and protect backup copies. Rotate ADMIN_TOKEN whenever operator access changes. Keep token distribution limited to authorized administrators only.

Step 5: Configure NGINX reverse proxy and TLS

server {
  listen 80;
  server_name vault.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name vault.example.com;

  ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;

  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 https;
    proxy_read_timeout 90;
  }
}

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Use your standard certificate workflow such as Certbot or centralized PKI automation. The strict redirect ensures credentials are never transmitted over plain HTTP after bootstrap.

Step 6: Launch stack and verify health gates

cd /opt/vaultwarden/compose
docker compose --env-file .env up -d
docker compose ps
docker compose logs --no-color --tail=80 vaultwarden
curl -I https://vault.example.com
curl -s https://vault.example.com/alive | jq . || true

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Confirm both container health and edge reachability before onboarding users. If the service is up but TLS fails, fix DNS and certificates first rather than bypassing HTTPS temporarily.

Step 7: Create first admin session and disable residual risk

# access admin panel
# https://vault.example.com/admin

# keep public registrations disabled
sed -i 's/^SIGNUPS_ALLOWED=.*/SIGNUPS_ALLOWED=false/' /opt/vaultwarden/compose/.env
docker compose --env-file /opt/vaultwarden/compose/.env up -d

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Keep public signups disabled unless you have a specific business requirement. For enterprise use, account creation should happen through controlled team processes.

Configuration and secrets handling best practices

Production safety for credential infrastructure depends more on process discipline than raw tooling. Treat Vaultwarden as a critical system and adopt repeatable controls:

  • Secret generation and rotation: rotate database passwords and admin tokens on a schedule and after staffing changes. Document rotation steps in your runbook to avoid brittle ad-hoc changes.
  • Backup encryption: encrypt backup artifacts before sending off-host. A plain SQL dump in object storage creates a second high-value breach target.
  • Access boundaries: restrict SSH and Docker access to a small operator group. Most incidents come from excessive operational privileges, not cryptographic weaknesses.
  • Update strategy: test image updates in staging, then roll to production with a rollback point. Never patch blindly on business-critical systems.
  • Audit readiness: log administrative actions and keep short incident notes. This context is valuable during compliance reviews and post-incident analysis.
  • Backup retention policy: keep daily and weekly backup windows with immutable storage where possible to reduce ransomware impact.

Verification checklist

  • HTTPS endpoint responds with valid certificate chain
  • Vaultwarden UI login works for expected accounts only
  • Database health check remains green after restart
  • New vault item create and read works end-to-end
  • Backups complete successfully and restore test passes in staging
  • No public exposure of PostgreSQL or container-internal ports
docker compose -f /opt/vaultwarden/compose/docker-compose.yml ps
sudo ss -tulpen | egrep '(:80|:443|:5432|:8080)'
curl -fsS https://vault.example.com/ >/dev/null && echo OK

If the copy button does not work in your browser, select the code block manually and copy with Ctrl/Cmd+C.

Common issues and fixes

1) 502 Bad Gateway from NGINX

This usually means Vaultwarden is not reachable on the expected local endpoint. Check container health and confirm your proxy target matches the exposed service port. Review Docker network and firewall rules if the target times out.

2) Login works but web vault feels slow

Look at host disk latency and PostgreSQL resource limits first. Password-manager workloads are small but sensitive to input output stalls. Slow disks and noisy neighbors can produce noticeable lag.

3) Certificate renewals succeed but site still serves old cert

Reload NGINX after renewal and verify file paths in your server block. Also check if an upstream load balancer is terminating TLS with its own cached certificate chain.

4) Backup files exist but restore fails

Backups are only valid when restore is tested. Run a scheduled restore drill to a temporary staging host and confirm real login plus item retrieval before declaring backup health.

5) WebSocket notifications are inconsistent

Verify reverse proxy headers and confirm no intermediary strips upgrade behavior. Keep NGINX and Docker networking simple before adding additional edge layers.

FAQ

Should I use SQLite instead of PostgreSQL for a small team?

SQLite can work for very small deployments, but PostgreSQL is the safer default for production operations. It gives better concurrency behavior, mature backup tooling, and easier observability as usage grows.

Can I keep Vaultwarden behind a VPN only?

Yes. For internal-only environments, VPN-only exposure is a strong option. Keep TLS enabled anyway and keep the same operational guardrails for backups, updates, and access logging.

How often should I rotate the admin token?

Rotate on staff change events and on a fixed schedule such as every 60 to 90 days. Immediate rotation is recommended if any operator endpoint may have been compromised.

What is the minimum backup schedule for production?

At minimum, daily encrypted database backups with retention and periodic restore tests. Teams with high credential churn often run more frequent snapshots plus weekly restore drills.

Do I need high availability for Vaultwarden?

Many teams begin with a single node and strong backups, then add high availability as dependency criticality increases. Decide based on business impact, recovery objectives, and operational maturity.

How do I reduce blast radius if the host is compromised?

Use strict operator access controls, isolate workloads, encrypt off-host backups, and maintain rapid rebuild playbooks. Fast rebuild plus tested restore is your strongest resilience pattern.

Can I integrate SSO later without full re-deploy?

Yes. Plan integration and policy changes as a controlled migration. Keep a rollback path and stage validation steps so user access continuity is preserved during cutover.

Related guides

Talk to us

Need help deploying and hardening production AI platforms, improving reliability, or building practical runbooks for your operations team? We can help with architecture, migration, security, and ongoing optimization.

Contact Us

Production Guide: Deploy Harbor with Kubernetes Helm and ingress-nginx on Ubuntu
A production-focused Harbor deployment with security hardening, TLS, RBAC, verification, and incident-ready troubleshooting.