Metabase is an open source business intelligence tool that lets you explore databases, build charts and share dashboards through a web interface, with a visual query builder for non-technical users and a SQL editor for everyone else. In this tutorial you will deploy Metabase on Ubuntu 24.04 with Docker Compose, store its own settings in PostgreSQL instead of the default embedded H2 file, publish it over HTTPS behind Nginx, connect a database with a read-only account and set up backups.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 GB of RAM (4 GB recommended for several concurrent users).
- A non-root user with
sudoprivileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository.
- A domain or subdomain (this guide uses
bi.your_domain) with a DNSArecord pointing toyour_server_ip. - Ports 22, 80 and 443 open in your firewall.
- A database you want to analyze (PostgreSQL, MySQL, MariaDB and others are supported) reachable from the server.
Step 1 - Preparing the project directory
Metabase needs its own database, called the application database, to store users, questions, dashboards and settings. Out of the box it uses an embedded H2 file, which the Metabase documentation does not recommend for production because it is easy to corrupt and hard to back up. This guide runs a PostgreSQL container for it instead.
Create the project directory:
sudo mkdir -p /opt/metabase
cd /opt/metabase
Generate a random password for the application database and store it in a .env file that Docker Compose reads automatically:
echo "MB_DB_PASS=$(openssl rand -hex 24)" | sudo tee /opt/metabase/.env > /dev/null
sudo chmod 600 /opt/metabase/.env
Confirm the file contains a single line with the password:
sudo cat /opt/metabase/.env
MB_DB_PASS=5f0c1e9a7b3d2c4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c
Step 2 - Writing the Compose file
Create the Compose file:
sudo nano /opt/metabase/compose.yaml
Add both services. Replace bi.your_domain in MB_SITE_URL with your domain:
services:
metabase-db:
image: postgres:17
container_name: metabase-db
restart: unless-stopped
environment:
POSTGRES_DB: metabase
POSTGRES_USER: metabase
POSTGRES_PASSWORD: ${MB_DB_PASS}
volumes:
- metabase-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabase -d metabase"]
interval: 10s
timeout: 5s
retries: 5
metabase:
image: metabase/metabase:latest
container_name: metabase
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase
MB_DB_PORT: 5432
MB_DB_USER: metabase
MB_DB_PASS: ${MB_DB_PASS}
MB_DB_HOST: metabase-db
MB_SITE_URL: https://bi.your_domain
JAVA_TIMEZONE: UTC
JAVA_OPTS: "-Xmx1g"
depends_on:
metabase-db:
condition: service_healthy
volumes:
metabase-db-data:
What the important settings do:
MB_DB_*point Metabase at the PostgreSQL container instead of H2. The PostgreSQL port is not published, so only Metabase can reach it.portsbinds Metabase to127.0.0.1. Ports published by Docker bypass UFW, so this is what keeps it off the internet until Nginx and TLS are in front.MB_SITE_URLis the public address Metabase uses in links in emails and embeds.JAVA_OPTScaps the Java heap. Around half of the server's RAM is a reasonable value; raise it to-Xmx2gon a 4 GB server.depends_onwithservice_healthymakes Metabase wait until PostgreSQL accepts connections.
TipFor a long-lived server, replace
latestwith a specific version tag from Docker Hub so upgrades happen only when you decide.
Validate the file:
sudo docker compose config --quiet && echo OK
OK
Step 3 - Starting Metabase
Start the stack:
sudo docker compose up -d
The first start takes one or two minutes while Metabase creates its tables in PostgreSQL. Follow the logs until initialization finishes, then press Ctrl+C:
sudo docker compose logs -f metabase
metabase | ... Metabase Initialization COMPLETE
Query the health endpoint:
curl -s http://127.0.0.1:3000/api/health
{"status":"ok"}
Step 4 - Publishing Metabase with Nginx and HTTPS
Install Nginx and Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create a server block:
sudo nano /etc/nginx/sites-available/metabase
server {
listen 80;
listen [::]:80;
server_name bi.your_domain;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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 300s;
}
}
proxy_read_timeout gives long-running queries time to finish instead of failing at the default 60 seconds.
Enable the site, allow web traffic and request a certificate:
sudo ln -s /etc/nginx/sites-available/metabase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d bi.your_domain
Check the public endpoint:
curl -s https://bi.your_domain/api/health
{"status":"ok"}
Step 5 - Completing the setup wizard
Open https://bi.your_domain in your browser. The setup wizard asks for:
- Your preferred language.
- The administrator account: name, email and a strong password.
- What you will use Metabase for, and optionally your first data source. You can skip the data source here and add it in the next step.
- Whether to share anonymous usage data with Metabase.
After the wizard you land on the Metabase home page, which includes a sample database you can use to try the query builder.
Step 6 - Connecting a data source with a read-only user
Metabase only needs to read your data. Giving it a dedicated read-only account limits the damage if the Metabase credentials ever leak, and prevents a SQL question from modifying data by accident.
On the database server, create the account. For PostgreSQL, connect with sudo -u postgres psql and run the following, replacing shop with your database name and your_ro_password with a strong password:
CREATE ROLE metabase_ro WITH LOGIN PASSWORD 'your_ro_password';
GRANT CONNECT ON DATABASE shop TO metabase_ro;
\c shop
GRANT USAGE ON SCHEMA public TO metabase_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO metabase_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO metabase_ro;
The last statement grants read access to tables created later by the same owner, so new tables appear in Metabase without extra grants.
Make sure the database accepts connections from the Metabase server: PostgreSQL must listen on a reachable address (listen_addresses in postgresql.conf), pg_hba.conf must allow metabase_ro from your_server_ip, and the database server's firewall must allow port 5432 from that IP only.
Then add it in Metabase:
- Click the gear icon and open Admin settings, then Databases.
- Click Add database and choose PostgreSQL.
- Fill in a display name, the host, port
5432, the database name,metabase_roand its password. - Click Save.
Metabase tests the connection before saving and then scans the schema. After a minute the tables appear under Browse data.
NoteIf the database runs on the same server as Metabase,
localhostinside the container refers to the container itself. Use the server's private IP address instead, and allow it inpg_hba.conf.
Step 7 - Building a question and a dashboard
With the data source in place, create a first question with the query builder:
- Click New and choose Question.
- Pick a table from your database.
- Add a filter, for example a date range, and a summary such as Count of rows grouped by a date column by month.
- Click Visualize, choose a chart type and Save the question.
For SQL users, New > SQL query opens the native editor. Variables in double braces become filter widgets:
SELECT date_trunc('month', created_at) AS month,
count(*) AS orders
FROM orders
WHERE created_at >= {{start_date}}
GROUP BY 1
ORDER BY 1;
After typing the query, set the variable type of start_date to Date in the side panel that opens.
To build a dashboard, click New > Dashboard, give it a name, add your saved questions and arrange the cards. Dashboard filters can be wired to several questions at once so one date picker controls every chart.
Control who sees what from Admin settings > People (users and groups) and Admin settings > Permissions (data access per group and collection access). Restrict the default All Users group and grant access through purpose-specific groups instead.
Step 8 - Backing up the application database
Everything you build in Metabase lives in the PostgreSQL container, so back it up. pg_dump produces a consistent dump while Metabase keeps running.
Create a backup script:
sudo nano /usr/local/bin/metabase-backup
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/metabase"
STAMP="$(date +%F)"
mkdir -p "$BACKUP_DIR"
docker exec metabase-db pg_dump -U metabase -d metabase \
| gzip > "$BACKUP_DIR/metabase-$STAMP.sql.gz"
# Keep 14 days of backups
find "$BACKUP_DIR" -name 'metabase-*.sql.gz' -mtime +14 -delete
Make it executable, run it once and check the result:
sudo chmod 750 /usr/local/bin/metabase-backup
sudo /usr/local/bin/metabase-backup
sudo ls -lh /var/backups/metabase
-rw-r--r-- 1 root root 1.8M Sep 25 13:10 metabase-2026-09-25.sql.gz
Schedule it nightly:
echo '15 3 * * * root /usr/local/bin/metabase-backup' | sudo tee /etc/cron.d/metabase-backup
Copy /var/backups/metabase to off-site storage as part of your regular backup routine.
Step 9 - Upgrading Metabase
Metabase migrates the application database automatically when a new version starts, and migrations cannot be rolled back. Always take a backup first:
sudo /usr/local/bin/metabase-backup
cd /opt/metabase
sudo docker compose pull metabase
sudo docker compose up -d
sudo docker compose logs -f metabase
Wait for Metabase Initialization COMPLETE again before using it. If the upgrade fails, restore the previous image tag and the dump taken just before the upgrade.
Troubleshooting
Nginx returns 502 Bad Gateway right after a restart. Metabase is still starting. Watch sudo docker compose logs -f metabase and wait for the initialization message.
Metabase fails to start with a database connection error. Check that metabase-db is healthy with sudo docker compose ps, and that MB_DB_PASS in .env has not changed since the PostgreSQL volume was created. PostgreSQL only applies POSTGRES_PASSWORD when the volume is first initialized.
The container is killed or logs java.lang.OutOfMemoryError. Raise -Xmx in JAVA_OPTS, but keep it below the server's free memory, then run sudo docker compose up -d. Check free -h to see how much RAM is available.
Dashboards load slowly. The time is usually spent in the source database. Add indexes on the columns used by filters and groupings, and enable result caching for heavy questions in the admin settings.
Conclusion
Metabase is now running on Ubuntu 24.04 with Docker Compose, backed by a PostgreSQL application database, served over HTTPS and connected to your data through a read-only account, with nightly backups. From here you can configure SMTP under the admin settings to send dashboard subscriptions and alerts, organize your work into collections with per-group permissions, and turn frequently used queries into models that other users can build on.
