Wong Edan's

Paranoid’s Paradise: Self-Hosted Bitwarden, Postgres, and n8n S3 Backups

February 27, 2026 • By Azzar Budiyanto

Greetings, you beautiful digital lunatics! Welcome to the inner sanctum of data sovereignty. If you are reading this, you’ve likely realized that trusting your most sensitive credentials to “The Cloud” is like giving your house keys to a giant, invisible neighbor who promises they won’t look inside your drawers. Sure, they have fancy locks, but it’s still their house. Today, we are reclaiming the throne. We are building a fortress of solitude for your passwords using the official Bitwarden Unified image, a robust PostgreSQL backend, and an automated backup pipeline using n8n that shoves your encrypted data into the S3 bucket of your choice. This isn’t just a tutorial; it’s an ascension. It’s Wong Edan style tech—crazy enough to work, brilliant enough to keep you safe.

The Philosophy of the Self-Hosted Fortress

Why go through all this trouble? Why not just use a standard SaaS password manager? Because, my fellow packet-pushers, we live in an era of “Data Serfdom.” When you host your own Bitwarden instance, you aren’t just a user; you are the Sovereign Lord of your Identity. But with great power comes the absolute certainty that you will eventually break something. That is why the Automated Daily Backup is the most critical part of this ritual. Without a backup, you aren’t a sysadmin; you’re just a gambler waiting for a disk failure to ruin your life.

We aren’t using Vaultwarden here. While Vaultwarden is a fantastic, lightweight Rust implementation, today we are going “Official.” We are using the Bitwarden Unified Docker image. Why? Because sometimes you want that enterprise-grade weight. You want the official support for features as they roll out. You want to feel the raw power of the same engine that powers massive corporations, right there on your cheap Raspberry Pi or your overkill homelab server.

The Architecture: The Holy Trinity of Data Safety

Our setup consists of three primary pillars:

  • The Core: Bitwarden Unified running in Docker. This handles the API, the Identity service, and the web vault.
  • The Memory: PostgreSQL. Instead of a flat file or a lightweight SQLite DB, we are using Postgres. It’s reliable, it’s scalable, and it makes our data feel like it’s wearing a tuxedo.
  • The Guardian: n8n. This is our automation “Big Brain.” It will wake up every night, poke the database, grab a snapshot, encrypt it if necessary, and fling it across the internet into an S3 bucket.

Prerequisites: Preparing the Ritual Circle

Before we start typing commands like a caffeinated monkey, you need a few things ready:

  • A Linux server (Ubuntu 22.04 or later is preferred, but any modern distro with Docker will do).
  • Docker and Docker Compose installed. If you don’t have these, stop reading and go find a basic “How to Linux” guide. We’ll wait.
  • A domain name with a reverse proxy (Nginx Proxy Manager, Traefik, or Caddy). Bitwarden requires HTTPS. No HTTPS, no vault. No exceptions.
  • An AWS S3 account (or any S3-compatible storage like Backblaze B2, MinIO, or DigitalOcean Spaces).
  • The sheer, unadulterated will to be the master of your own destiny.

Phase 1: The Docker Compose Manifest

We start by creating a directory for our masterpiece. Let’s call it /opt/bitwarden-ultimate. Inside this directory, we will create our docker-compose.yml file. This file is our digital blueprint.


version: '3.8'

services:
  bitwarden:
    image: bitwarden/self-host:beta
    container_name: bitwarden-unified
    restart: always
    environment:
      - BW_DB_PROVIDER=postgresql
      - BW_DB_SERVER=db
      - BW_DB_DATABASE=bitwarden_db
      - BW_DB_USERNAME=postgres
      - BW_DB_PASSWORD=YourSuperSecretPassword
      - BW_INSTALLATION_ID=YOUR_INSTALLATION_ID
      - BW_INSTALLATION_KEY=YOUR_INSTALLATION_KEY
      - BW_ENABLE_ADMIN=true
    volumes:
      - ./bw-data:/etc/bitwarden
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    container_name: bitwarden-db
    restart: always
    environment:
      - POSTGRES_DB=bitwarden_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=YourSuperSecretPassword
    volumes:
      - ./db-data:/var/lib/postgresql/data

  n8n:
    image: docker.n8n.io/n8nio/n8n
    container_name: n8n-automation
    restart: always
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=n8n_password
    volumes:
      - ./n8n-data:/home/node/.n8n
      - /var/run/docker.sock:/var/run/docker.sock # Only if you're feeling adventurous

Deep Dive into the Environment Variables

Do not just copy-paste that and run away! Look at BW_DB_PROVIDER. We are explicitly telling Bitwarden to look for a PostgreSQL instance. The BW_INSTALLATION_ID and BW_INSTALLATION_KEY must be obtained from the official Bitwarden host site. They use this to notify you of updates and provide a sense of legitimacy to your self-hosted instance. It’s free, so don’t be a cheapskate—go get them.

Notice the db service. We are using the alpine version of Postgres because we aren’t made of RAM. The volumes ./bw-data and ./db-data ensure that when you restart your container, your passwords don’t vanish into the digital ether. If you lose these folders, you lose your life. Back them up. Love them.

Phase 2: Igniting the Engines

Run the command: docker-compose up -d. Now, watch the logs like a hawk: docker-compose logs -f bitwarden. The first boot might take a moment as Postgres initializes and Bitwarden runs its migrations. If you see “Database is ready,” you may proceed to do a little victory dance. But don’t dance too long; we haven’t secured the perimeter yet.

Access your Bitwarden instance via your reverse proxy URL. Create an account. This is your vault. Once in, go to the /admin panel. This is where you can disable new registrations. DO THIS IMMEDIATELY. Unless you want every bot on the internet using your server to store their stolen credentials, close the gates.

Phase 3: The n8n Brain – Setting Up the Backup Pipeline

Now we get to the “Wong Edan” magic. We could use a boring cron job. We could write a bash script that we’ll forget exists. But no, we want visibility. We want n8n. Access your n8n instance (port 5678 by default). We are going to build a workflow that does the following:

The Workflow Logic

  1. Schedule Trigger: Every day at 03:00 AM (the hour of the wolf).
  2. Execute Command: Run a pg_dump inside the Postgres container to create a backup file.
  3. Read Binary File: Grab that .sql file and bring it into the n8n environment.
  4. S3 Upload: Send that file to an S3 bucket with a timestamp.
  5. Notification (Optional): Send a message to Discord or Telegram saying “Master, the data is safe.”

Step 3.1: The pg_dump Node

Inside n8n, drag an “Execute Command” node. The command will look something like this:

docker exec bitwarden-db pg_dump -U postgres bitwarden_db > /home/node/backup.sql

Note: For this to work, n8n needs permission to talk to the Docker socket or the backup must be written to a shared volume. The cleanest way is to have the DB container dump the file into a volume that n8n can also see. Let’s adjust our docker-compose slightly to include a /backups volume shared between db and n8n.

Step 3.2: The S3 Node

Configure the S3 node with your Access Key, Secret Key, and Region. For the “Object Key,” use a dynamic expression like: bitwarden_backup_{{ $now.format('YYYY-MM-DD_HH-mm') }}.sql. This ensures you have a history of backups and aren’t just overwriting the same file. Overwriting is for losers. History is for legends.

Phase 4: Hardening the Database

PostgreSQL is a beast, but it’s a beast that needs to be caged. By default, our docker-compose has it in the same network as Bitwarden. This is good. But ensure that your db service does NOT have any ports exposed to the outside world. No 5432:5432 in your compose file unless you want a database administrator in North Korea to have a fun afternoon. The only container that should talk to db is bitwarden and n8n.

Consider adding a health check to your Postgres container. This ensures that Bitwarden doesn’t try to start before the database is ready to receive connections. Bitwarden can be quite impatient, much like a toddler waiting for candy.


healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s
  timeout: 5s
  retries: 5

Phase 5: The “Wong Edan” Disaster Recovery Drill

A backup is just a collection of useless bits until you’ve proven you can restore it. This is where most people fail. Don’t be “most people.”

To test your recovery, create a fresh Docker environment. Pull your .sql file from S3. Run the Postgres container. Inject the backup using: cat backup.sql | docker exec -i bitwarden-db psql -U postgres bitwarden_db. Then, spin up the Bitwarden container. If your passwords appear, you are a god among men. If not, you’ve got work to do, and I suggest you do it before your main server dies.

Why Postgres over the default?

You might ask, “Why did we leave the simplicity of the default setup?” Because PostgreSQL allows for Point-In-Time Recovery (PITR) if you’re feeling truly insane. It allows for better handling of concurrent connections if you’re sharing your instance with a large family or a small cult. It’s about professional-grade reliability in a home-grade setting.

Advanced Automation: Pruning the S3 Bucket

If you keep uploading 100MB backups every day, your S3 bill will eventually grow larger than your ego. Use n8n to check the bucket and delete files older than 30 days. Or, even better, set up an S3 Lifecycle Policy on the bucket itself to automatically move old backups to “Glacier” storage (where they are cheap and cold) and then delete them after 90 days. Efficiency is the hallmark of the Wong Edan mindset.

Security Considerations: The Final Layer

Since you are hosting your own passwords, your server is now the highest-value target in your house. Here are the non-negotiables:

  • SSH Keys Only: Disable password authentication for SSH. If you’re still using a password to log into your server, we can’t be friends.
  • Fail2Ban: Install it. Configure it. Let it feast on the IP addresses of the bots trying to brute-force your door.
  • Automatic Security Updates: Use unattended-upgrades on Ubuntu. You don’t want to get hacked because you forgot to patch a kernel vulnerability from three months ago.
  • Encryption at Rest: If possible, run your Docker volumes on an encrypted LUKS partition. If someone physically steals your server, they shouldn’t get your database.

Conclusion: The Peace of Mind

There you have it. You’ve built a self-healing, automated, high-availability (well, high-reliability) password vault. You have the official Bitwarden experience, the power of PostgreSQL, and the automated oversight of n8n. You can sleep soundly knowing that even if your house burns down, your passwords are safely tucked away in an encrypted S3 bucket, waiting for you to resurrect them.

Go forth, configure your vault, and remember: in the world of self-hosting, the only person you can truly trust is yourself—and perhaps a well-written backup script. Stay crazy, stay secure!