How To Install Bitwarden on Ubuntu 26.04 LTS

Install Bitwarden on Ubuntu 26.04

Password managers fail in two ways. Some are convenient and quietly leak your data to someone else’s cloud. Others are locked down so hard that nobody on the team uses them, and credentials end up in spreadsheets and chat threads. Self-hosting Bitwarden sits between those two. You keep the vault on hardware you control, and people still get browser extensions, mobile apps and a clean web interface.

Ubuntu 26.04 LTS, codenamed Resolute Raccoon, shipped on 23 April 2026. The Ubuntu project lists standard support running until May 2031. That makes it a sensible base for a vault you want to leave alone for years. Bitwarden’s official Linux installer is a Bash script that drives Docker, so the work is mostly about preparing the server properly and then keeping it healthy.

This guide covers the whole path on a fresh Ubuntu 26.04 LTS server: choosing the right deployment type, installing Docker, creating a dedicated user, running the installer, setting up SMTP, hardening the host, backing up, and updating. It finishes with the failures that actually show up in production. The commands also work on Debian 12/13 with small changes, and the concepts carry over to AlmaLinux with dnf instead of apt.

Which Bitwarden Deployment Should You Choose?

Bitwarden offers more than one self-hosted option. Picking the wrong one is the most common early mistake.

The standard Linux deployment uses the bitwarden.sh script. It runs the full stack in Docker, with a bundled MSSQL Express database by default, or an external database if you prefer. The official minimums are a 1.4 GHz x64 processor, 2 GB RAM, 12 GB storage and Docker Engine 26+ with Compose. Treat 2 GB as the floor, not a target. A 4 GB server is much more comfortable.

Bitwarden Lite is the single-container option. It needs only about 200 MB of RAM, 1 GB of storage and Docker Engine 26+. It was called Bitwarden Unified during beta. In December 2025 it exited beta and was renamed Lite, and the image became ghcr.io/bitwarden/lite. If you were running the old beta image, update the image name or your upgrades will stall.

Factor Standard (bitwarden.sh) Lite
Containers Multiple services behind bundled Nginx Single image
Minimum RAM 2 GB ~200 MB
Database MSSQL Express by default, external optional External options such as SQLite, MySQL, PostgreSQL or MSSQL
Best for Teams, organizations, full feature set Small teams, homelabs, low-resource VPS
Maintenance Script-driven (update, renewcert) Image pull and restart

This guide follows the standard script-based path, since it is the one Bitwarden documents most thoroughly and the one most teams need. If you run a small family vault on a 1 GB VPS, look at Lite instead. Do not fight the SQL Server container on a machine that cannot hold it.

Prerequisites and Planning

Skip this section and you will pay for it later in troubleshooting. Bitwarden’s own checklist is short:

  1. A domain name with DNS pointing at the server, and ports 80 and 443 open.
  2. Docker and Docker Compose installed.
  3. A dedicated user and directory for the installation.
  4. An installation ID and key from bitwarden.com/host.

A few practical notes from running this kind of stack.

Use a real hostname. Something like vault.example.com works. Create the A record (and AAAA if you use IPv6) before you start. Let’s Encrypt validation will fail if DNS has not propagated, and repeated failures can trip rate limits.

Give it its own machine or VM. Bitwarden ships its own Nginx container that binds to ports 80 and 443. If the host already runs Apache or Nginx on those ports, you either need a reverse proxy setup (covered later) or a separate server. A password vault is also the wrong thing to share a box with a dozen WordPress sites. A compromise of one plugin should not put your credential store in the blast radius.

Plan the hardware. For a team of up to roughly 50 users, 2 vCPU and 4 GB RAM with an SSD is comfortable. MSSQL is the hungry component, and it caches aggressively. Slow disks hurt more than slow CPUs here.

Get your installation credentials. Visit bitwarden.com/host, enter an email address, pick the US or EU data region, and copy the installation ID and key. These identify your server to Bitwarden’s licensing and push notification services. Treat the key like a password. Never paste it into a ticket or a public repo.

Prepare SMTP. Bitwarden uses email for account verification, invitations and passwordless admin portal login. Have SMTP host, port, username and password ready before you begin. Using a transactional provider beats relaying through a mailbox on the same server.

Step 1: Prepare Ubuntu 26.04 LTS

Log in over SSH and bring the system fully up to date. Do this first, and reboot if a new kernel arrives.

sudo apt update
sudo apt full-upgrade -y
sudo apt install -y ca-certificates curl gnupg ufw unattended-upgrades
sudo reboot

Confirm what you are running:

lsb_release -a
uname -r
free -h
df -hT /

You should see Ubuntu 26.04 and the Resolute codename. Check the memory and free disk figures against the requirements above. If you are on a cloud VPS with 2 GB of RAM, add a swap file as a safety net. It will not make SQL Server fast, but it can stop the kernel’s OOM killer from taking out a container during a spike.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

Setting vm.swappiness low tells the kernel to use swap only under real pressure, which is what you want on a database host.

Set the hostname and timezone while you are here. Correct time matters more than people expect. TOTP two-factor codes and TLS validation both depend on an accurate clock.

sudo hostnamectl set-hostname vault.example.com
sudo timedatectl set-timezone Asia/Jakarta
timedatectl status

Use your own timezone, of course. Make sure System clock synchronized: yes appears in the output.

Step 2: Configure the Firewall

Allow SSH first, then HTTP and HTTPS. Order matters. Enabling UFW before allowing SSH is how people lock themselves out of remote machines.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Port 80 stays open because Let’s Encrypt uses the HTTP challenge for validation and renewal. The Bitwarden Nginx container will redirect normal traffic to HTTPS.

Here is a caveat that catches experienced admins too. Docker writes its own iptables (or nftables) rules for published ports, and those can bypass UFW’s rules. For Bitwarden this is mostly harmless, because the only published ports are 80 and 443 and you want those open anyway. It becomes a problem if you later run other containers with published ports on the same host and assume UFW protects them. It does not, reliably. Verify with an external scan from another machine:

nmap -Pn -p- your_server_ip

Only SSH, 80 and 443 should show as open.

Step 3: Install Docker Engine and Compose

The Bitwarden installer requires Docker Engine and the Compose plugin. Use Docker’s official repository rather than the older distro-packaged versions. The Bitwarden docs call for Engine 26 or newer, and the official repo keeps you well beyond that.

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

That sequence mirrors the pattern used in published Ubuntu guides for Bitwarden. On a brand new LTS release, third-party repositories sometimes lag by days or weeks. If apt update complains that the repository has no Release file for resolute, check whether Docker has published packages for that codename. If it has not, a temporary fallback is pointing the repo line at the previous LTS codename (noble). That works in practice for Docker’s statically linked binaries, but switch back once the proper suite appears. Do not leave that workaround forgotten in a production box for two years.

Verify the installation:

sudo systemctl enable --now docker
docker --version
docker compose version
sudo docker run --rm hello-world

Both docker and docker compose should respond. The Bitwarden script itself checks for docker compose first and falls back to the legacy docker-compose binary if needed, so the plugin is the cleaner choice.

Step 4: Create a Dedicated Bitwarden User

Do not run the installer as root, and do not run it from your personal home directory. A dedicated account keeps file ownership predictable and makes audits simpler.

sudo adduser bitwarden
sudo groupadd -f docker
sudo usermod -aG docker bitwarden
sudo mkdir -p /opt/bitwarden
sudo chmod -R 700 /opt/bitwarden
sudo chown -R bitwarden:bitwarden /opt/bitwarden

Be honest about what the docker group means. Any member can effectively become root on the host, because they can mount the filesystem into a container. The Bitwarden user is therefore not a low-privilege account in the strict sense. The purpose here is separation and tidiness, not a security boundary. Give the account a strong password or, better, no SSH login at all. You can reach it with sudo -iu bitwarden from your admin account.

sudo -iu bitwarden
cd /opt/bitwarden

Step 5: Download and Run the Installer

Now the actual installation. Download the script from Bitwarden and make it executable:

curl -Lso bitwarden.sh "https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" && chmod 700 bitwarden.sh

Before running anything you downloaded from the internet, read it. It is a short wrapper that fetches a second script (run.sh) and exposes the supported commands: install, start, restart, stop, update, updatedb, updaterun, updateself, updateconf, uninstall, renewcert and rebuild.

less bitwarden.sh
./bitwarden.sh install

The installer will create a ./bwdata directory relative to the script location. Then it asks a series of questions:

  • Domain name: the fully qualified name, for example vault.example.com.
  • Let’s Encrypt: answer y if the server is public and port 80 is reachable, then supply an email address for expiry notices.
  • Database name: anything simple, such as vault.
  • Installation ID and key: the values from bitwarden.com/host.
  • Region: US or EU, matching what you picked on the website.
  • Certificate choice: if you decline Let’s Encrypt, the script asks whether you have your own certificate or want a self-signed one.

Pulling the images takes a few minutes depending on bandwidth. When it finishes, start the stack:

./bitwarden.sh start

First start is slow. SQL Server initializes, migrations run, and the containers become healthy one after another. Watch progress in a second terminal:

docker ps --format 'table {{.Names}}\t{{.Status}}'

You are looking for every bitwarden-* container to show (healthy). Give it a good two to five minutes before assuming anything is broken. Impatience at this stage causes more false alarms than real faults.

Step 6: Configure Environment Settings and SMTP

The installer leaves generated configuration under bwdata. Two files matter most: bwdata/config.yml for installer-level settings and bwdata/env/global.override.env for application settings such as email.

Edit the override file:

nano ~/bwdata/env/global.override.env

(If you installed in /opt/bitwarden, the path is /opt/bitwarden/bwdata/env/global.override.env.)

Fill in the SMTP block with your provider’s details:

globalSettings__mail__replyToEmail=no-reply@example.com
globalSettings__mail__smtp__host=smtp.example.com
globalSettings__mail__smtp__port=587
globalSettings__mail__smtp__ssl=false
globalSettings__mail__smtp__username=smtp_user
globalSettings__mail__smtp__password=smtp_password
adminSettings__admins=you@example.com

Port 587 normally uses STARTTLS, so ssl=false is correct there. For implicit TLS on port 465, set ssl=true. Getting this pairing wrong is the top cause of “emails never arrive” tickets. The adminSettings__admins line grants access to the system administrator portal at /admin, which authenticates by emailing a login link. That is why SMTP must work before you rely on it.

Apply changes by restarting:

./bitwarden.sh restart

Then open https://vault.example.com, create your first account, and verify that the verification email arrives. Do this immediately. If email is broken, you want to know now, not during an employee onboarding.

After creating your own account, consider disabling open registrations so strangers who find the URL cannot sign up. In the same override file:

globalSettings__disableUserRegistration=true

Restart again afterward. Invitations from an organization still work when open signup is off, which is usually exactly the behavior a private vault needs.

Step 7: Put It Behind an Existing Reverse Proxy (Optional)

Sometimes ports 80 and 443 are already taken, or company policy requires a central proxy. Bitwarden supports this, but the installer’s bundled Nginx must move to alternate ports and stop handling TLS itself.

In bwdata/config.yml, adjust values along these lines:

http_port: 8080
https_port: 8443
ssl: false

Then rebuild so the generated configuration reflects the change:

./bitwarden.sh rebuild
./bitwarden.sh restart

On the front-end proxy, forward traffic with the correct headers. A minimal Nginx server block looks like this:

server {
    listen 443 ssl http2;
    server_name vault.example.com;

    ssl_certificate     /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

    client_max_body_size 525m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 300;
    }
}

The generous client_max_body_size matters for file attachments and Send uploads. Without it you will see mysterious 413 errors. Always confirm the exact config keys against your installed version of config.yml, since the file’s contents vary with release. Test with sudo nginx -t before reloading.

Step 8: Harden the Server

A vault server is a high-value target. A few measures give real protection for little effort.

Lock down SSH. Use keys, disable password login and root login:

sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
sudo sshd -t && sudo systemctl reload ssh

Keep your current session open and test a second login before closing it. Always.

Add Fail2ban for SSH.

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Turn on automatic security updates. Unattended upgrades handle OS patches, but they do not update Bitwarden containers. That is a separate job, covered below.

sudo dpkg-reconfigure --priority=low unattended-upgrades

Protect the data directory. bwdata holds database files, certificates and secrets in env/. Keep permissions tight:

sudo chmod -R go-rwx /opt/bitwarden/bwdata
sudo chown -R bitwarden:bitwarden /opt/bitwarden

Enforce two-factor authentication on the admin account at minimum, and ideally for every user. Hardware keys or TOTP both work. A self-hosted vault with a weak master password and no second factor is only as safe as the weakest user.

Check TLS. After installation, run the domain through an SSL test and confirm only modern protocols are enabled. Bitwarden’s bundled Nginx ships sensible defaults, but verify rather than assume.

Performance Tuning

Bitwarden is not a heavy workload on its own. Vault reads and writes are small. Most slowness traces back to the database container and the disk beneath it.

Memory. SQL Server will happily use whatever it can find. On a 4 GB server shared with other services, that leads to swapping and sluggish syncs. Watch real usage:

docker stats --no-stream
free -h

If memory is tight, upgrade the instance before attempting clever tuning. The documentation’s 2 GB minimum exists for a reason.

Disk I/O. Use SSD or NVMe storage. Check latency during a busy period:

sudo apt install -y sysstat iotop
iostat -x 2 5

High await values with low throughput point at an overloaded or throttled virtual disk, which is common on cheap shared VPS plans.

CPU. Master password hashing happens on the client, so the server does not burn cycles on KDF operations. Spikes usually come from many devices syncing at once, for instance Monday morning when a whole office unlocks vaults. Two vCPUs handle that for typical team sizes.

Network. Keep latency low between users and the server. If your team is spread across regions, consider hosting near the majority. Sync payloads are small, but round trips add up on mobile connections.

Log growth. Container logs can quietly eat disk. Cap them in Docker’s daemon config:

sudo nano /etc/docker/daemon.json
{
  "log-driver": "local",
  "log-opts": { "max-size": "20m", "max-file": "5" }
}
sudo systemctl restart docker
cd /opt/bitwarden && ./bitwarden.sh restart

Existing containers pick up new logging settings only when they are recreated, so the restart step is not optional.

Backups That Actually Restore

A vault without tested backups is a liability, not a service. A reliable plan covers two things: the bwdata directory (configuration, certificates, attachments) and the database.

Bitwarden’s MSSQL container writes backups into the data directory under bwdata/mssql/backups. The simplest dependable approach is stopping the stack briefly and archiving everything:

cd /opt/bitwarden
./bitwarden.sh stop
sudo tar --numeric-owner -czf /backup/bitwarden-$(date +%F).tar.gz bwdata
./bitwarden.sh start

A short downtime window overnight is fine for most teams. If you need zero downtime, take MSSQL-native backups from inside the database container and copy those out, plus a file-level copy of bwdata excluding live database files.

Automate it with a systemd timer or cron, ship the archives off-host (restic, rclone or plain rsync to another machine), and encrypt them. Backups contain encrypted vault data, but they also contain server secrets and your installation key.

Then do the part nobody likes. Restore the backup onto a throwaway VM and log in. Do it quarterly. A backup you have never restored is a hope, not a plan.

Updating Bitwarden Safely

Updates are where self-hosting earns its keep or loses it. Bitwarden’s script handles both the script itself and the containers:

cd /opt/bitwarden
./bitwarden.sh updateself
./bitwarden.sh update

Run updateself first so you have the latest installer logic, then update to pull new images and migrate the database. Always take a backup immediately before. Never update on a Friday afternoon. Read release notes for breaking changes, especially around database migrations and configuration keys.

To renew a Let’s Encrypt certificate manually:

./bitwarden.sh renewcert

The update process renews certificates when they are close to expiry, so a regular maintenance cycle every few weeks keeps TLS healthy. Put a calendar reminder or monitoring check on certificate expiry anyway:

echo | openssl s_client -servername vault.example.com -connect vault.example.com:443 2>/dev/null | openssl x509 -noout -dates

Monitoring and Day-to-Day Operations

A few commands cover most routine checks.

docker ps
docker logs bitwarden-api --tail 100
docker logs bitwarden-identity --tail 100
docker logs bitwarden-mssql --tail 100
sudo ss -tlnp | grep -E ':80|:443'
df -h /opt/bitwarden

For uptime monitoring, point an external checker (Uptime Kuma, Healthchecks or your preferred service) at https://vault.example.com/alive. A simple HTTP 200 check from outside your network catches DNS, certificate and firewall problems that internal checks miss.

Also watch disk space with an alert at 80 percent. Full disks corrupt databases in ugly ways.

Troubleshooting Common Problems

Containers stay “unhealthy” or keep restarting

Most often this is memory. Check docker logs bitwarden-mssql. If SQL Server reports insufficient memory, the host is too small. Add RAM, close competing services, or switch to Bitwarden Lite with an external database.

Let’s Encrypt fails during install

Verify three things: DNS resolves to this server’s public IP, port 80 is reachable from the internet, and nothing else is listening on port 80.

dig +short vault.example.com
sudo ss -tlnp | grep ':80'
curl -I http://vault.example.com

If a cloud provider’s security group blocks 80, UFW being open does not help. Check both layers.

“Port is already allocated” or bind errors

Another service holds 80 or 443. Identify it with sudo ss -tlnp, stop or move it, or use the reverse proxy approach described earlier. Bitwarden runs its own Nginx, so do not run a second web server on those ports.

Emails are not arriving

Re-check host, port and the SSL flag pairing. Test connectivity from the server:

openssl s_client -starttls smtp -connect smtp.example.com:587

Also check spam folders and your provider’s sending logs. Many cloud hosts block outbound port 25 by default, which is another reason to use 587 with a relay.

Cannot reach the admin portal

The email in adminSettings__admins must match your account email exactly. Restart after editing the file. If the login email never arrives, you are back to the SMTP issue above.

413 Request Entity Too Large

Applies mainly to reverse proxy setups. Raise client_max_body_size in the front-end proxy.

Docker repository errors on a new release

If apt update fails for Docker’s repository on Ubuntu 26.04, the codename suite may not be published yet. Check the repository index for resolute, and use the temporary noble fallback only as a stopgap.

Installer says Docker Compose not found

The script looks for docker compose, then docker-compose, and exits if neither exists. Confirm the plugin installed with docker compose version.

Clients show a certificate warning

A self-signed certificate triggers this on every device. Use Let’s Encrypt or a certificate from your internal CA, and distribute the CA to clients if you go the private route.

Slow sync after months of use

Look at disk latency, container memory and database size. Rebuilding indexes and ensuring adequate RAM often fixes it. Large attachment libraries also bloat backups, so consider limits.

r00t is an experienced Linux enthusiast and technical writer with a passion for open-source software. With years of hands-on experience in various Linux distributions, r00t has developed a deep understanding of the Linux ecosystem and its powerful tools. He holds certifications in SCE and has contributed to several open-source projects. r00t is dedicated to sharing her knowledge and expertise through well-researched and informative articles, helping others navigate the world of Linux with confidence.

Related Posts