If you have been self-hosting Vaultwarden using the default SQLite database, you may have encountered limitations as your user base expanded. While SQLite is efficient for single-user and small household applications, it struggles under the pressure of concurrent writes from multiple users, leading to slow synchronization, timeouts, and the dreaded “database is locked” errors. This guide is designed to assist Canadian IT administrators and self-hosting enthusiasts in transitioning their Vaultwarden installation from SQLite to PostgreSQL, thereby enabling multi-user organizations, group policies, and robust backup solutions—essentially bridging the gap between a personal vault and a production-ready password manager for teams.
Why Move Vaultwarden From SQLite to PostgreSQL
SQLite, being a single-file, serverless database, is the default choice for Vaultwarden due to its zero-configuration setup. However, its write-locking mechanism allows only one write transaction at a time across the entire database file. This limitation is negligible for small households but becomes a significant bottleneck for larger teams, resulting in failed vault item saves and delays in administrative tasks.
In contrast, PostgreSQL offers a client-server architecture that supports concurrent connections and row-level locking, making it a more suitable choice for teams. Vaultwarden has long supported PostgreSQL through its Diesel ORM layer, and the official documentation provides clear guidance on the necessary connection string syntax. Organizations experiencing performance issues with SQLite are directed toward PostgreSQL or MariaDB as a viable solution.
Moreover, PostgreSQL enhances data durability with features like point-in-time recovery, streaming replication for high availability, and established backup tools such as pg_dump. These capabilities are crucial for organizations that rely on Vaultwarden to store sensitive credentials.
Where to Run This: VPS Options for Canadian Teams
When deploying this stack, the choice of hosting provider can significantly impact latency and simplify data residency discussions. Below is a comparison of typical monthly costs in CAD for VPS options suitable for running Vaultwarden alongside a PostgreSQL container:
| Provider | Plan Size | Approx. Monthly Cost (CAD) | Canadian Data Centre |
|---|---|---|---|
| DigitalOcean | 2 vCPU / 4GB RAM | ~ | Toronto (TOR1) |
| Vultr | 2 vCPU / 4GB RAM | ~ | Toronto |
| OVHcloud | 2 vCPU / 4GB RAM | ~ | Beauharnois, QC |
| Linode (Akamai) | 2 vCPU / 4GB RAM | ~ | Toronto |
| Self-hosted on-prem hardware | N/A (existing hardware) | Electricity only | Your own office/closet |
A VPS with 2 vCPU and 4GB RAM provides ample resources for Vaultwarden and PostgreSQL, accommodating up to approximately 50 users. For larger teams, consider upgrading to 4 vCPU and 8GB RAM to prevent resource contention during peak usage.
Prerequisites and Versions You’ll Need
Before initiating the migration, ensure the following prerequisites are met to avoid common pitfalls:
- An existing Vaultwarden deployment running version 1.34 or later (this guide is tested against 1.37.1) with a functioning SQLite database (
db.sqlite3) - Docker Engine 27.x or later and Docker Compose v2.29 or later on a Linux host (preferably Ubuntu 24.04 LTS or Debian 12)
- A PostgreSQL server, version 16 or 17, either as a Docker container (
postgres:16-alpine) or a managed instance pgloaderversion 3.6.x or later installed on the host- Root or sudo access to the server, plus SSH access if the server is remote
- A verified, working backup of your current
vw-datavolume - A maintenance window of 60 to 90 minutes where users are asked to close their Vaultwarden clients
- At least 2 GB of free disk space for the temporary export and the new PostgreSQL data directory
No new domain or reverse proxy configuration is required for this migration, as PostgreSQL operates alongside your existing Caddy or Nginx setup.
Step 1 — Back Up Your Existing Vaultwarden Data Volume
Backing up your data is crucial. Stop the container and archive the entire data directory to ensure you capture all necessary files, including rsa_key.pem and attachments.
cd /opt/vaultwarden
docker compose stop vaultwarden
tar -czf vw-backup-$(date +%Y%m%d-%H%M).tar.gz ./vw-data
ls -lh vw-backup-*.tar.gz
Store this backup securely off the host. In case of any issues during the migration, this archive serves as your rollback plan.
Step 2 — Deploy a PostgreSQL Container Alongside Vaultwarden
Add a PostgreSQL service to your existing compose.yaml file, ensuring it operates on the same internal Docker network as Vaultwarden.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
Start the database container first and confirm it initializes correctly before proceeding with Vaultwarden.
docker compose up -d vw-db
docker compose logs -f vw-db
# wait for: "database system is ready to accept connections"
Step 3 — Set the DATABASE_URL Environment Variable
Update the .env file with the new DATABASE_URL variable, formatted according to PostgreSQL URI standards. Do not restart Vaultwarden yet; the schema needs to be populated first.
# .env
DATABASE_URL=postgresql://vaultwarden:REPLACE_WITH_A_STRONG_PASSWORD@vw-db:5432/vaultwarden
ADMIN_TOKEN=$argon2id$v=19$m=65540,t=3,p=4$...
SIGNUPS_ALLOWED=false
WEBSOCKET_ENABLED=true
Ensure any special characters in the database password are percent-encoded to avoid parsing errors.
Step 4 — Let Vaultwarden Create the Empty PostgreSQL Schema
Start Vaultwarden with the new PostgreSQL connection to allow it to create the necessary schema. This step is solely for schema generation; your vault data remains in SQLite.
docker compose up -d vaultwarden
docker compose logs -f vaultwarden
# look for: "Migrations to run" followed by no errors
Once Vaultwarden starts successfully, stop it again to prepare for the data migration.
docker compose stop vaultwarden
Step 5 — Install pgloader and Prepare the SQLite Source File
Install pgloader from the PostgreSQL APT repository to ensure you have the latest version. Disable Write-Ahead Logging mode on the SQLite database to allow for a consistent snapshot during migration.
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt install -y pgloader
pgloader --version
sqlite3 ./vw-data/db.sqlite3 "PRAGMA journal_mode=DELETE;"
Step 6 — Run the pgloader Migration
Create a pgloader command file to load data from SQLite into PostgreSQL, excluding the Diesel migration tracking table.
cat > vw-migrate.load 'EOF'
LOAD DATABASE
FROM sqlite:///opt/vaultwarden/vw-data/db.sqlite3
INTO postgresql://vaultwarden:REPLACE_WITH_A_STRONG_PASSWORD@localhost:5432/vaultwarden
WITH data only, include no drop, reset sequences
EXCLUDING TABLE NAMES LIKE '__diesel_schema_migrations'
SET work_mem to '32MB', maintenance_work_mem to '128MB';
EOF
pgloader vw-migrate.load
After running pgloader, check the summary output for any discrepancies in row counts. If errors occur, verify your connection string and table names before retrying.
Step 7 — Start Vaultwarden on PostgreSQL and Verify Data Integrity
Bring Vaultwarden back online and check the logs for a successful startup. Log in with existing accounts to ensure all data has migrated correctly.
docker compose up -d vaultwarden
docker compose logs --tail=50 vaultwarden
Log in as multiple users to confirm that all vault items, folders, and organizations are intact before notifying your team to resume using the service.
Step 8 — Create Organizations for Team-Based Vault Sharing
With PostgreSQL now managing concurrent writes effectively, you can create organizations for shared vault access. This feature is available in self-hosted Vaultwarden without requiring a paid Bitwarden license.
- Navigate to Organizations → New Organization in the web vault.
- Name the organization according to your team or department.
- Create Collections to categorize credentials by project or client.
- Invite members via email, even with
SIGNUPS_ALLOWED=false. - Assign roles based on the principle of least privilege.
Step 9 — Configure Group Policies for Consistent Team Security
Establish security policies within your organization to enforce a baseline level of security for all members.
- Set minimum master password requirements under Organization Settings → Policies.
- Enable two-step login enforcement for added security.
- Consider implementing “Single Organization” policies to restrict membership to your organization only.
- Configure “Personal Ownership” restrictions to direct new items into the organization rather than personal vaults.
Step 10 — Automate PostgreSQL Backups
Unlike SQLite, PostgreSQL requires a structured backup approach. Implement a cron job to automate pg_dump backups, ensuring regular data protection.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
Test the restore process periodically to ensure your backups are reliable and effective.
Step 11 — Tune PostgreSQL for Your Team Size
Adjust PostgreSQL settings to optimize performance for your user base. The default settings may not suffice for larger teams.
| Setting | Default | Recommended for 10–50 Users | Why It Matters |
|---|---|---|---|
| shared_buffers | 128MB | 256MB | Enhances caching, reducing disk reads during sync |
| max_connections | 100 | 50 | Lowering this frees host memory for other processes |
| work_mem | 4MB | 16MB | Improves query performance and sorting operations |
| effective_cache_size | 4GB | 1GB (adjust to host RAM) | Helps optimize query planning |
| wal_compression | off | on | Reduces WAL file size, saving backup storage costs |
Step 12 — Monitor Database Health Going Forward
PostgreSQL provides valuable metrics for monitoring. Regularly check active connection counts and database size, and set up alerts for disk space usage to avoid unexpected outages.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
For those already utilizing Prometheus and Grafana, integrating postgres_exporter can provide detailed insights into database performance.
SQLite vs PostgreSQL for Vaultwarden: Which One Fits Your Team
Not every deployment necessitates PostgreSQL. The following table serves as a practical guide for selecting the appropriate backend based on team size and usage patterns:
| Deployment Type | Recommended Backend | Typical Symptom If Wrong Choice | Backup Method |
|---|---|---|---|
| Single user | SQLite (default) | None — SQLite is already ideal | File copy of vw-data |
| Household (2–5 people) | SQLite (default) | Rare, occasional lock timeout | File copy of vw-data |
| Small team (6–20 people) | PostgreSQL | Slow sync, admin panel hangs at peak hours | pg_dump + attachment archive |
| Department (20–100 people) | PostgreSQL, tuned | Frequent “database is locked” errors | pg_dump + WAL archiving |
| Enterprise (100+ people, HA required) | PostgreSQL with replication | Downtime risk from single point of failure | Streaming replication + pg_dump |
How Long the Migration Actually Takes
The duration of the migration primarily hinges on the size of your vault. The following table outlines expected runtimes based on real-world experiences:
| Vault Size | Approx. Users | pgloader Runtime | Recommended Maintenance Window |
|---|---|---|---|
| Small | Under 10 | Under 30 seconds | 30 minutes |
| Medium | 10–50 | 1–3 minutes | 60 minutes |
| Large | 50–200 | 3–10 minutes | 90 minutes |
| Very large, many attachments | 200+ | 10–30 minutes | 2 hours, scheduled outside business hours |
While the pgloader step is typically swift, the verification process is where most time should be allocated. Ensure thorough testing by logging in with multiple accounts and checking for data integrity.
High Availability: Running Vaultwarden Behind a Load Balancer
For teams requiring zero downtime, consider implementing a highly available setup with PostgreSQL streaming replication and multiple Vaultwarden instances behind a load balancer.
- Establish a primary PostgreSQL instance with one or more streaming replicas.
- Point multiple Vaultwarden containers at the same primary
DATABASE_URL. - Share the
/datavolume across instances using NFS or shared block storage. - Utilize HAProxy or Nginx to distribute HTTP traffic and manage failover.
Complete Working Project: The Full Stack in One Place
To simplify the setup, here’s a consolidated view of the necessary files for a complete working project:
compose.yaml — the full stack configuration for Vaultwarden, PostgreSQL, and Caddy for automatic HTTPS.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
Caddyfile — configuration for automatic HTTPS with Let’s Encrypt.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
.env — environment variables for the Vaultwarden container.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
With these files in place, the entire stack can be launched with a single command, providing a secure, PostgreSQL-backed Vaultwarden deployment ready for team use.
SSO and LDAP for Larger Teams
As your self-hosted vault scales, managing user accounts manually becomes impractical. Vaultwarden’s SSO support allows integration with existing identity providers, streamlining account lifecycle management.
- Enable SSO by setting
SSO_ENABLED=trueand configuring the necessary parameters for your OIDC provider. - Register Vaultwarden as an OIDC client application with the correct redirect URI.
- Conduct tests with a non-critical account before rolling out to the entire organization.
- Maintain a local master-password login option for administrative fallback during outages.
Disaster Recovery: Running a Full Restore Drill
Regularly testing your backup strategy is essential. Conduct a full restore drill on a temporary host to verify your backups and the restoration process.
services:
vw-db:
image: postgres:16-alpine
container_name: vw-postgres
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_PASSWORD"
volumes:
- ./vw-postgres-data:/var/lib/postgresql/data
networks:
- vw-net
vaultwarden:
image: vaultwarden/server:1.37.1-alpine
container_name: vaultwarden
restart: unless-stopped
depends_on:
- vw-db
environment:
DOMAIN: "https://vault.example.ca"
env_file:
- .env
volumes:
- ./vw-data:/data
networks:
- vw-net
networks:
vw-net:
driver: bridge
After confirming the user count matches expectations, document the successful drill date for future reference.
Common Pitfalls When Migrating Vaultwarden to PostgreSQL
Here are some common mistakes to avoid during the migration process:
- Running pgloader while Vaultwarden is still connected to SQLite. Always stop the container before migration to prevent inconsistent exports.
- Forgetting to percent-encode special characters in the database password. This can lead to connection string parsing errors.
- Neglecting to let Vaultwarden create the schema first. An empty database will cause migration errors.
- Failing to exclude the Diesel migrations table from the pgloader load. This can disrupt future updates.
- Assuming the migration is reversible without a backup. Always have a backup before proceeding.
- Exposing the PostgreSQL port to the host network. Keep it internal for security.
- Underestimating the maintenance window for large vaults. Allocate sufficient time for larger migrations.
Troubleshooting Guide: 8 Common Errors and Fixes
Here are some common errors you may encounter during or after migration, along with their solutions:
- “Error parsing DATABASE_URL” — Check for unescaped special characters in the password.
- “relation does not exist” during pgloader — Ensure the Vaultwarden schema was created first.
- “database is locked” persists after migration — Verify
DATABASE_URLpoints to PostgreSQL. - Vaultwarden starts but the vault appears empty — Recheck the migration process and connection string.
- pgloader reports “connection refused” — Ensure the PostgreSQL container is healthy.
- WebSocket sync stops working after migration — Confirm
WEBSOCKET_ENABLED=trueis set in.env. - Attachments show as broken links — Verify that the
/datavolume contains the actual files. - Admin panel login fails after migration — Check the
ADMIN_TOKENin your.envfile.
Advanced Tips for Running Vaultwarden at Team Scale
Once your migration is complete, consider these advanced configurations:
- Pin your Vaultwarden image to a specific version tag to avoid unexpected updates.
- Enable SMTP for email notifications regarding new device logins and invitations.
- Set
PUSH_ENABLED=truefor faster mobile sync notifications. - Review diagnostics after PostgreSQL upgrades to catch schema mismatches early.
- Rotate your
ADMIN_TOKENperiodically and store it securely.
Vaultwarden vs Official Bitwarden Self-Hosted Server for Teams
Understanding the trade-offs between Vaultwarden and Bitwarden’s official self-hosted server is essential for making an informed decision:
| Factor | Vaultwarden (Community) | Bitwarden Official Self-Hosted |
|---|---|---|
| Resource footprint | Single lightweight container, tens of MB RAM | Multi-container stack, several GB RAM recommended |
| License cost | Free, MIT-licensed | Free tier available; paid tiers for Organizations at scale |
| Vendor support | Community-driven (GitHub, Discourse) | Official Bitwarden support channels |
| Database options | SQLite, PostgreSQL, MySQL/MariaDB | MSSQL by default in the official stack |
| Setup complexity | Single Docker Compose file | Multiple services, install script required |
Frequently Asked Questions
Do I have to migrate to PostgreSQL if my Vaultwarden team is small?
No, SQLite is sufficient for small teams. Migrate when you experience performance issues.
Will my users notice anything during the migration?
Users will only notice a brief downtime during the migration process.
Can I migrate directly from SQLite to PostgreSQL without pgloader?
While possible, it is not recommended due to the complexity and potential for errors.
Is MySQL/MariaDB a better choice than PostgreSQL for Vaultwarden?
Both are viable options, but PostgreSQL is often preferred for its stability and tooling.
Do I need a paid Bitwarden license to use Organizations on self-hosted Vaultwarden?
No, Vaultwarden supports Organizations for free.
What happens if the pgloader migration fails partway through?
Your SQLite file remains intact, allowing you to revert back easily.
How often should I run pg_dump backups for a team Vaultwarden instance?
Nightly backups are recommended for shared credential environments.
Can I run this migration on a managed PostgreSQL service instead of a Docker container?
Yes, as long as the Vaultwarden container can access it on the network.