Running your own password manager on a small VPS is one of the best value-for-money homelab projects in 2026. Vaultwarden is a lightweight, compatible implementation of the Bitwarden server API — and paired with Caddy as a reverse proxy, you get automatic HTTPS with almost no configuration overhead.
This guide walks through a production-ready setup on a 1GB RAM VPS using Ubuntu 24.04 LTS. Example domain: vault.example.com.
What you need
- A VPS with 1 vCPU / 1GB RAM (Hetzner CX22, DigitalOcean Basic, Vultr, etc.)
- A domain you control (e.g.
example.com)- SSH access as root or a sudo user
- Roughly 30 minutes
Architecture Overview
Internet │ ▼┌─────────────────────────────────────┐│ VPS (1GB RAM) ││ ││ Caddy :443 ──► Vaultwarden :80 ││ (TLS/HTTPS) (Docker internal) ││ ││ /opt/vaultwarden/data (SQLite DB) │└─────────────────────────────────────┘Caddy terminates TLS and forwards traffic to Vaultwarden. Vaultwarden never exposes port 80 to the public internet directly.
Step 1: Initial Server Hardening
SSH into your VPS and update the system:
apt update && apt upgrade -yapt install -y curl wget git ufw fail2ban unattended-upgradesCreate a non-root admin user:
adduser deployusermod -aG sudo deploymkdir -p /home/deploy/.sshcp ~/.ssh/authorized_keys /home/deploy/.ssh/chown -R deploy:deploy /home/deploy/.sshchmod 700 /home/deploy/.sshchmod 600 /home/deploy/.ssh/authorized_keysConfigure the firewall — only SSH and web ports:
ufw default deny incomingufw default allow outgoingufw allow OpenSSHufw allow 80/tcpufw allow 443/tcpufw enableufw status verboseDisable root SSH login Edit
/etc/ssh/sshd_config, setPermitRootLogin noandPasswordAuthentication no, then runsystemctl restart sshd. Always test SSH in a second terminal before closing your current session.
Step 2: Memory Tuning for 1GB RAM
A 1GB VPS runs Vaultwarden comfortably, but you should add swap and reduce unnecessary memory pressure.
# Create a 1GB swap filefallocate -l 1G /swapfilechmod 600 /swapfilemkswap /swapfileswapon /swapfile
# Persist across rebootsecho '/swapfile none swap sw 0 0' >> /etc/fstab
# Lower swappiness (use RAM first, swap only when needed)echo 'vm.swappiness=10' >> /etc/sysctl.confsysctl -pVerify:
free -hswapon --showExpected output on a fresh 1GB VPS with swap:
total used freeMem: 961Mi 250Mi 600MiSwap: 1.0Gi 0B 1.0GiStep 3: Install Docker
curl -fsSL https://get.docker.com | shsystemctl enable --now docker
# Allow deploy user to run docker without sudousermod -aG docker deployLog out and back in as deploy, then verify:
docker --versiondocker compose versionStep 4: DNS Configuration
Point your subdomain to the VPS public IP before starting Caddy (required for Let’s Encrypt HTTP-01 validation).
| Type | Name | Value | TTL |
|---|---|---|---|
| A | vault | 203.0.113.10 | 300 |
Replace 203.0.113.10 with your actual VPS IP. Example FQDN: vault.example.com.
Verify DNS propagation:
dig +short vault.example.com# Expected: 203.0.113.10Step 5: Project Directory Structure
sudo mkdir -p /opt/vaultwarden/{data,caddy/data,caddy/config}sudo chown -R deploy:deploy /opt/vaultwardencd /opt/vaultwardenFinal layout:
/opt/vaultwarden/├── docker-compose.yml├── Caddyfile├── .env├── data/ # Vaultwarden SQLite + attachments└── caddy/ ├── data/ # TLS certificates (auto-managed) └── config/Step 6: Environment Variables
Create /opt/vaultwarden/.env:
# DomainDOMAIN=vault.example.com
# Vaultwarden admin panel (generate a strong token)# openssl rand -base64 48ADMIN_TOKEN=CHANGE_ME_TO_A_LONG_RANDOM_STRING
# Signups: set to false after creating your accountSIGNUPS_ALLOWED=true
# Optional: restrict registration to specific email domain# INVITATIONS_ALLOWED=trueGenerate a secure admin token:
openssl rand -base64 48Copy the output into ADMIN_TOKEN.
Step 7: Caddyfile
Create /opt/vaultwarden/Caddyfile:
{ # Email for Let's Encrypt expiry notices email [email protected]
# Recommended for small VPS servers { protocols h1 h2 }}
vault.example.com { # Security headers header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" Referrer-Policy "strict-origin-when-cross-origin" -Server }
# Vaultwarden WebSocket notifications (required for live sync) @websockets { path /notifications/hub path /notifications/hub/negotiate } reverse_proxy @websockets vaultwarden:3012
# All other traffic reverse_proxy vaultwarden:80}Replace the domain Change every occurrence of
vault.example.comand[email protected]to match your real domain and contact email.
Step 8: Docker Compose
Create /opt/vaultwarden/docker-compose.yml:
services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped env_file: .env environment: DOMAIN: "https://${DOMAIN}" ADMIN_TOKEN: "${ADMIN_TOKEN}" SIGNUPS_ALLOWED: "${SIGNUPS_ALLOWED}" WEBSOCKET_ENABLED: "true" # Keep logs small on low-RAM VPS LOG_LEVEL: warn EXTENDED_LOGGING: "false" # Disable unused features to save RAM SHOW_PASSWORD_HINT: "false" volumes: - ./data:/data networks: - vaultnet # Memory limit — safe ceiling for 1GB VPS deploy: resources: limits: memory: 256M
caddy: image: caddy:2-alpine container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" # HTTP/3 volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy/data:/data - ./caddy/config:/config networks: - vaultnet depends_on: - vaultwarden deploy: resources: limits: memory: 128M
networks: vaultnet: driver: bridgeStart the stack:
cd /opt/vaultwardendocker compose pulldocker compose up -ddocker compose psdocker compose logs -f --tail=50Wait until Caddy obtains a certificate. You should see lines like:
caddy | certificate obtained successfullyStep 9: Create Your Account
- Open
https://vault.example.comin a browser. - Click Create account and register with your email.
- Install the Bitwarden client (desktop, browser extension, or mobile app).
- In client settings, set Custom server URL to
https://vault.example.com. - Log in with the account you just created.
After registration, lock down signups:
cd /opt/vaultwardensed -i 's/SIGNUPS_ALLOWED=true/SIGNUPS_ALLOWED=false/' .envdocker compose up -dProtect your admin token The admin panel is at
https://vault.example.com/admin. Never share yourADMIN_TOKENor commit.envto version control.
Step 10: Automated Backups
Vaultwarden stores everything in /opt/vaultwarden/data/. Back up this directory daily.
Create /opt/vaultwarden/backup.sh:
#!/usr/bin/env bashset -euo pipefail
BACKUP_DIR="/opt/vaultwarden/backups"DATA_DIR="/opt/vaultwarden/data"TIMESTAMP=$(date +%Y%m%d-%H%M%S)ARCHIVE="${BACKUP_DIR}/vaultwarden-${TIMESTAMP}.tar.gz"
mkdir -p "${BACKUP_DIR}"
# Stop writes briefly for a consistent SQLite snapshotdocker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwardentar -czf "${ARCHIVE}" -C "${DATA_DIR}" .docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwarden
# Keep last 14 daysfind "${BACKUP_DIR}" -name "vaultwarden-*.tar.gz" -mtime +14 -delete
echo "Backup saved: ${ARCHIVE}"Make it executable and schedule with cron:
chmod +x /opt/vaultwarden/backup.sh
# Daily at 03:00(crontab -l 2>/dev/null; echo "0 3 * * * /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1") | crontab -Off-site backups Sync
/opt/vaultwarden/backups/to S3, Backblaze B2, or another server withrclone. Your password vault is only as safe as your backups.
Step 11: Health Checks & Maintenance
# Container statusdocker compose -f /opt/vaultwarden/docker-compose.yml ps
# Live resource usagedocker stats --no-stream
# Update images (monthly)cd /opt/vaultwardendocker compose pulldocker compose up -d
# View logsdocker compose logs -f vaultwardendocker compose logs -f caddyTypical memory usage on a 1GB VPS after setup:
| Service | RAM Usage |
|---|---|
| Vaultwarden | ~30–80 MB |
| Caddy | ~15–40 MB |
| OS + Docker | ~200–350 MB |
| Headroom | ~500 MB+ |
Troubleshooting
Certificate not issued
# Check DNS firstdig +short vault.example.com
# Caddy logsdocker compose logs caddy | grep -i error
# Ensure ports 80/443 are opensudo ufw statusWebSocket / live sync not working
Confirm the Caddyfile includes the @websockets block pointing to port 3012, and that WEBSOCKET_ENABLED=true is set in the Vaultwarden environment.
Out of memory (OOM)
# Check if OOM killer struckdmesg | grep -i "killed process"
# Confirm swap is activefree -hIf OOM persists, lower Docker memory limits or upgrade to a 2GB VPS.
Security Checklist
- SSH key-only authentication enabled
- UFW allows only ports 22, 80, 443
-
SIGNUPS_ALLOWED=falseafter account creation - Strong
ADMIN_TOKENstored securely - Daily encrypted off-site backups configured
- Bitwarden clients use
https://vault.example.comas server URL - 2FA enabled on your Vaultwarden account
Conclusion
You now have a self-hosted password manager that:
- Costs roughly $4–6/month on a 1GB VPS
- Uses ~100–150 MB RAM under normal load
- Gets automatic HTTPS via Caddy
- Stays compatible with all official Bitwarden clients
Next steps Enable 2FA in your vault, set up off-site backups with
rclone, and consider adding CrowdSec in front of Caddy for additional brute-force protection.