Vaultwarden

Here is how I set up Vaultwarden (a Bitwarden-compatible server) on K3s, using a budget VPS with 1 vCPU and 2GB of RAM. The whole thing costs me about $2 a month.

Why Vaultwarden and K3s?

Vaultwarden is what Bitwarden could be if it were built for simplicity and efficiency. It is written in Rust, fully compatible with Bitwarden clients, and skips all the complexity of the official Bitwarden setup. You do not need Docker Compose with ten containers or a ton of memory. Vaultwarden runs as a single binary with barely any dependencies.

K3s, Rancher’s lightweight Kubernetes, fits right in for this kind of deployment. My whole stack runs comfortably on a low-end VPS with Ubuntu 24.04.

The stack

Vaultwarden handles the vault itself, and K3s runs it. For the database I use NeonDB, a free managed PostgreSQL service; SQLite also works, but Postgres makes backups easier and leaves room to grow. A Cloudflare Tunnel fronts everything, so the VPS never exposes a public port and its IP stays private.

All requests go through Cloudflare, then the tunnel, land on your VPS, and finally hit Vaultwarden. Passwords are stored on your VPS disk, and all other metadata sits in NeonDB.

Storage

For single-node setups, simple is best. I use hostPath volumes so Vaultwarden writes directly to a local folder on the VPS. Backups are as easy as copying that folder. The only tradeoff is that the pod is tied to this node, but in a single-node cluster this is exactly what you want.

Networking

I use a NodePort service. No need for an Ingress controller since Cloudflare handles SSL and all public access. Cloudflare Tunnel means nothing is exposed publicly, and your server’s IP stays private.

The Kubernetes YAML

PersistentVolume

apiVersion: v1
kind: PersistentVolume
metadata:
  name: vaultwarden-pv
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /opt/vaultwarden-data
    type: DirectoryOrCreate

PersistentVolumeClaim

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vaultwarden-data
  namespace: vaultwarden
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: ""
  selector:
    matchLabels:
      app: vaultwarden

Secrets

apiVersion: v1
kind: Secret
metadata:
  name: vaultwarden-secrets
  namespace: vaultwarden
type: Opaque
stringData:
  DATABASE_URL: "postgresql://user:pass@your.neon.db.url/vaultwarden?sslmode=require"
  DOMAIN: "https://vault.yourdomain.tld"

Deployment Keep replicas: 1 and use Recreate as the strategy. Make sure the folder on your VPS is owned by user 1000 and group 1000, which matches Vaultwarden’s container defaults.

Setup

  1. Prepare the VPS:

    sudo mkdir -p /opt/vaultwarden-data
    sudo chown 1000:1000 /opt/vaultwarden-data
    sudo chmod 755 /opt/vaultwarden-data
  2. Apply manifests in order:

    kubectl apply -f namespace.yaml
    kubectl apply -f pv.yaml
    kubectl apply -f pvc.yaml
    kubectl apply -f secret.yaml
    kubectl apply -f deployment.yaml
    kubectl apply -f service.yaml
  3. Check that it’s running:

    kubectl get pods -n vaultwarden -w
    kubectl logs -n vaultwarden deployment/vaultwarden

Maintenance

  • Update Vaultwarden:

    kubectl set image deployment/vaultwarden vaultwarden=vaultwarden/server:1.XX.X -n vaultwarden
    kubectl rollout status deployment/vaultwarden -n vaultwarden
  • Monitor:

    kubectl get pods -n vaultwarden
    kubectl top pods -n vaultwarden
    kubectl logs -n vaultwarden deployment/vaultwarden --tail=50

What it costs

ItemMonthly Cost
VPS (1vCPU, 2GB)$1
Domain$1
NeonDB$0 (free tier)
Cloudflare$0

That’s a full Bitwarden-compatible setup for about $2 per month, and maintenance amounts to the couple of kubectl commands above. If you’ve been meaning to own your own credentials, this is a very doable weekend project.