5 min read

Self-Hosted Vaultwarden & Caddy on a 1GB RAM VPS: Secure, Lightweight, and Cost-Effective

Step-by-step guide to deploying Vaultwarden behind Caddy with automatic HTTPS on a budget 1GB RAM VPS — including Docker Compose, firewall hardening, backups, and memory tuning.

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:

initial-setup.sh
apt update && apt upgrade -y
apt install -y curl wget git ufw fail2ban unattended-upgrades

Create a non-root admin user:

create-admin-user.sh
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Configure the firewall — only SSH and web ports:

ufw-setup.sh
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose

Disable root SSH login Edit /etc/ssh/sshd_config, set PermitRootLogin no and PasswordAuthentication no, then run systemctl 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.

swap-setup.sh
# Create a 1GB swap file
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# Persist across reboots
echo '/swapfile none swap sw 0 0' >> /etc/fstab
# Lower swappiness (use RAM first, swap only when needed)
echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -p

Verify:

Terminal window
free -h
swapon --show

Expected output on a fresh 1GB VPS with swap:

total used free
Mem: 961Mi 250Mi 600Mi
Swap: 1.0Gi 0B 1.0Gi

Step 3: Install Docker

install-docker.sh
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
# Allow deploy user to run docker without sudo
usermod -aG docker deploy

Log out and back in as deploy, then verify:

Terminal window
docker --version
docker compose version

Step 4: DNS Configuration

Point your subdomain to the VPS public IP before starting Caddy (required for Let’s Encrypt HTTP-01 validation).

TypeNameValueTTL
Avault203.0.113.10300

Replace 203.0.113.10 with your actual VPS IP. Example FQDN: vault.example.com.

Verify DNS propagation:

Terminal window
dig +short vault.example.com
# Expected: 203.0.113.10

Step 5: Project Directory Structure

create-project-dirs.sh
sudo mkdir -p /opt/vaultwarden/{data,caddy/data,caddy/config}
sudo chown -R deploy:deploy /opt/vaultwarden
cd /opt/vaultwarden

Final 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:

.env
# Domain
DOMAIN=vault.example.com
# Vaultwarden admin panel (generate a strong token)
# openssl rand -base64 48
ADMIN_TOKEN=CHANGE_ME_TO_A_LONG_RANDOM_STRING
# Signups: set to false after creating your account
SIGNUPS_ALLOWED=true
# Optional: restrict registration to specific email domain
# INVITATIONS_ALLOWED=true

Generate a secure admin token:

Terminal window
openssl rand -base64 48

Copy the output into ADMIN_TOKEN.

Step 7: Caddyfile

Create /opt/vaultwarden/Caddyfile:

Caddyfile
{
# Email for Let's Encrypt expiry notices
# 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.com and [email protected] to match your real domain and contact email.

Step 8: Docker Compose

Create /opt/vaultwarden/docker-compose.yml:

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: bridge

Start the stack:

start-services.sh
cd /opt/vaultwarden
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=50

Wait until Caddy obtains a certificate. You should see lines like:

caddy | certificate obtained successfully

Step 9: Create Your Account

  1. Open https://vault.example.com in a browser.
  2. Click Create account and register with your email.
  3. Install the Bitwarden client (desktop, browser extension, or mobile app).
  4. In client settings, set Custom server URL to https://vault.example.com.
  5. Log in with the account you just created.

After registration, lock down signups:

disable-signups.sh
cd /opt/vaultwarden
sed -i 's/SIGNUPS_ALLOWED=true/SIGNUPS_ALLOWED=false/' .env
docker compose up -d

Protect your admin token The admin panel is at https://vault.example.com/admin. Never share your ADMIN_TOKEN or commit .env to version control.

Step 10: Automated Backups

Vaultwarden stores everything in /opt/vaultwarden/data/. Back up this directory daily.

Create /opt/vaultwarden/backup.sh:

backup.sh
#!/usr/bin/env bash
set -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 snapshot
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar -czf "${ARCHIVE}" -C "${DATA_DIR}" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwarden
# Keep last 14 days
find "${BACKUP_DIR}" -name "vaultwarden-*.tar.gz" -mtime +14 -delete
echo "Backup saved: ${ARCHIVE}"

Make it executable and schedule with cron:

schedule-backup.sh
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 with rclone. Your password vault is only as safe as your backups.

Step 11: Health Checks & Maintenance

maintenance-commands.sh
# Container status
docker compose -f /opt/vaultwarden/docker-compose.yml ps
# Live resource usage
docker stats --no-stream
# Update images (monthly)
cd /opt/vaultwarden
docker compose pull
docker compose up -d
# View logs
docker compose logs -f vaultwarden
docker compose logs -f caddy

Typical memory usage on a 1GB VPS after setup:

ServiceRAM Usage
Vaultwarden~30–80 MB
Caddy~15–40 MB
OS + Docker~200–350 MB
Headroom~500 MB+

Troubleshooting

Certificate not issued

Terminal window
# Check DNS first
dig +short vault.example.com
# Caddy logs
docker compose logs caddy | grep -i error
# Ensure ports 80/443 are open
sudo ufw status

WebSocket / 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)

Terminal window
# Check if OOM killer struck
dmesg | grep -i "killed process"
# Confirm swap is active
free -h

If 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=false after account creation
  • Strong ADMIN_TOKEN stored securely
  • Daily encrypted off-site backups configured
  • Bitwarden clients use https://vault.example.com as 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.

Found this article helpful? Share it