Workload · Running team dashboards

Metabase self hosted: your team's dashboards on a server you control

To self-host Metabase properly you need two pieces on your server: Metabase itself, run from the official Docker image or the JAR file, and a PostgreSQL database where Metabase saves your dashboards, questions and user accounts. Metabase's own docs warn against leaving that in the built-in H2 file once real people depend on it. Below you will size the machine, start both pieces with Docker Compose, make yourself the admin privately, and, last of all, let your team in over HTTPS.

The short version
  1. Docker Compose: Metabase plus PostgreSQL 16
  2. Secrets in env files only root can read
  3. Admin account made through an SSH tunnel first
  4. nginx and a free certificate in front of port 3000
  5. Nightly dump of the app database, kept off the box

What you are actually running

Metabase talks to two kinds of database. Your business data, the sales tables or event logs you want charts of, stays where it already is; Metabase connects to it and asks it questions. Everything Metabase creates for you, from saved questions to dashboards, collections, people and permissions, goes into its application database.

Out of the box that application database is H2, a single file. It is there so you can try Metabase on a laptop. Metabase's docs recommend PostgreSQL for production and list MySQL 8.4.0 or MariaDB 10.6.0 as alternatives. Pick PostgreSQL at the start and you never face the one-off load-from-h2 move later.

If your reporting data lives in PostgreSQL too, the PostgreSQL workload page helps you run that side well, and Metabase can also chart a ClickHouse database when your event tables grow large.

How big a server does Metabase need?

Memory is the number that matters most, because Metabase runs inside Java. Metabase's troubleshooting guide explains that Java grabs roughly a quarter of the machine's RAM by default and keeps holding it. You can give it more with a setting called -Xmx (in Docker, JAVA_OPTS=-Xmx2g and similar), as long as 1 to 2 GB stays free for the rest of the system.

Server RAMHeap setting from Metabase's example
2 GB-Xmx1g
4 GB-Xmx2g
MoreKeep going the same way, leaving 1 to 2 GB free

That example assumes Metabase is alone. Here PostgreSQL sits beside it, so our advice is to hold back extra memory for the database before you raise the heap. If charts slow down and memory use over time looks like a saw blade, with constant cleanup cycles, Metabase is short of heap.

This is where a whole machine helps. On a Primcast dedicated server Java sees memory nobody else is using, the port is unmetered, and you have root. If your reporting data grows, you can add disks to the machine you already have, with no migration and no new address.

Up and running in an evening

You need Ubuntu plus a Docker Engine release from 28 onward, with its Compose plugin, both from Docker's own apt repository. Become root with sudo -i, then paste the block. It switches ufw on before Metabase or PostgreSQL starts (IPv6 enabled first, so the allow rules get IPv6 copies), writes two env files that only root can read (the strict umask 077 applies only while they are written, then goes back to 022), and starts the two containers.

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw --force enable
umask 077
mkdir -p /srv/metabase && cd /srv/metabase
PW=$(openssl rand -hex 24)
cat > pg.env <<EOF
POSTGRES_USER=metabase
POSTGRES_DB=metabaseappdb
POSTGRES_PASSWORD=$PW
EOF
cat > metabase.env <<EOF
MB_DB_TYPE=postgres
MB_DB_HOST=postgres
MB_DB_PORT=5432
MB_DB_DBNAME=metabaseappdb
MB_DB_USER=metabase
MB_DB_PASS=$PW
MB_ENCRYPTION_SECRET_KEY=$(openssl rand -base64 32)
MB_SITE_URL=https://dash.example.com
EOF
unset PW
chmod 600 pg.env metabase.env
umask 022
cat > compose.yaml <<'EOF'
services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    env_file: pg.env
    volumes:
      - pgdata:/var/lib/postgresql/data
  metabase:
    image: metabase/metabase:latest
    restart: unless-stopped
    env_file: metabase.env
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      - postgres
volumes:
  pgdata:
EOF
docker compose up -d
  • Why heredocs: the passwords never appear as command arguments, so they stay out of ps and your shell history.
  • No quotes in the env files: keep values free of quotes and spaces, which the generated ones are.
  • Why 127.0.0.1: a port written as plain 3000:3000 is published on every address the server has, IPv4 and IPv6, and Docker's IPv4 rules bypass ufw (and on IPv6 as well if Docker manages IPv6 itself). Binding to 127.0.0.1 keeps Metabase reachable from the server only, which Docker Engine 28 and later enforce strictly (moby#45610). PostgreSQL is not published at all.
  • The encryption key: MB_ENCRYPTION_SECRET_KEY (16 characters or more) makes Metabase encrypt the logins it stores for your data sources. Lose it and you type those connections in again, so store a second copy of metabase.env away from the server.

Prefer no Docker? Metabase also ships as a JAR that runs on Java 25 as a systemd service, reading the same MB_DB_* settings from an environment file. The Docker route is the one Metabase's docs recommend for self-hosting. Later, swap latest for a fixed release tag so you choose when versions change.

Make yourself the admin before anyone else can

The first person through Metabase's setup screens becomes its admin, with the power to add users and connect databases. So do it now, while only the server itself can reach port 3000.

  1. On your own computer, run ssh -N -L 9000:127.0.0.1:3000 you@your-server, using your normal login.
  2. Open http://localhost:9000, press Let's get started and create your admin account.
  3. Connect your business database with a read-only user that sees only the tables you want charted, which is what Metabase's security guide advises. You can also add data later.
  4. Close the tunnel. Now you are ready to give it a public address.

Giving your team a proper https address

Metabase's security guide says not to expose its port directly but to put a reverse proxy in front, and its own service guide uses nginx for that. Point your DNS name at the server first, then run as root; certbot requests a free certificate from Let's Encrypt and switches the site to https.

apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/dash <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name dash.example.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF
ln -s /etc/nginx/sites-available/dash /etc/nginx/sites-enabled/dash
nginx -t && systemctl reload nginx
certbot --nginx -d dash.example.com

The X-Forwarded-Proto line tells Metabase the visitor arrived over https, which the security guide asks the proxy to send. Because MB_SITE_URL already holds your https address, links in emails point to the right place. When you change anything in metabase.env, apply it with docker compose up -d from /srv/metabase.

Keeping it safe: backups and upgrades

Everything you build in Metabase lives in that one PostgreSQL database, so a nightly dump protects all of it. The dumps go into a folder inside the private /srv/metabase directory, so create it with mkdir /srv/metabase/dumps, then save this one line as the file /etc/cron.d/metabase-dump:

30 3 * * * root umask 077; cd /srv/metabase && docker compose exec -T postgres pg_dump -U metabase -Fc metabaseappdb > /srv/metabase/dumps/metabase-$(date +\%F).dump

A dump that lives on the server's own disks is lost with them, so copy these files to another place on a schedule of your own, together with metabase.env. You manage the server; we keep the hardware and the network running. Try a restore now and then, so you know it works.

Upgrading is short. Metabase's upgrade guide starts with a backup of the application database, so run the dump line by hand first. Coming from a version older than 40? Upgrade one major version at a time until you reach the latest 40, then jump.

  1. Before upgrading, run the dump line by hand, check which version you are on in docker compose logs metabase, and keep the current image under a second name: docker image tag metabase/metabase:latest metabase-before-upgrade. Pulling replaces latest, so this tag is what you go back to. (Metabase's guide prefers a fixed release tag over latest; with one pinned in compose.yaml, that tag is your way back.)
  2. Upgrade with docker compose pull metabase and docker compose up -d. Metabase updates its own tables when it starts.
  3. If it goes wrong, stop Metabase with docker compose stop metabase. Start from an empty application database, so nothing the newer version added is left behind: docker compose exec --no-TTY postgres dropdb --username=metabase metabaseappdb, then docker compose exec --no-TTY postgres createdb --username=metabase metabaseappdb. Then load the dump from before the upgrade: cat dumps/metabase-2026-10-23.dump | docker compose exec --no-TTY postgres pg_restore --dbname=metabaseappdb --username=metabase (use your own file, from /srv/metabase).
  4. In compose.yaml change the Metabase image to metabase-before-upgrade (or your earlier tag), then start it again with docker compose up -d. You are back on the old version with the old data.

Is self-hosted Metabase free?

The Open Source edition you just installed is, licensed with the AGPL (the Affero GPL). Metabase's licence page puts the condition plainly: "If you want to use the code, you must abide by the AGPL." The paid Pro and Enterprise plans use a different build (the metabase/metabase-enterprise image) licensed through the Metabase Commercial Software License, and you activate a licence token before their extras switch on. As the same page says: "Though Metabase Enterprise Edition is source available, without the commercial license, you can't use any of the Enterprise features."

Planning to show Metabase charts inside your own app? Embedding has its own terms on the licence page, one of them on condition that the "Powered by Metabase" logo stays visible. Read them before you build on it.

Questions people ask

Can I run Metabase with the H2 file it starts with?
Only to try it out. The docs tell you to keep H2 out of production, and on Docker the file sits inside the container, so removing the container removes your dashboards. Start with PostgreSQL, as the setup above does.
What happens to my data when I upgrade Metabase?
On startup a new version migrates its application database to the new layout automatically. That is why the upgrade guide begins with a backup: if you need the old version back after a major upgrade, restore that backup and start the previous image, as the steps above show. The new version on the restored data would simply migrate it forward again.
Which port does Metabase use?
Port 3000 by default. In the setup above it is only reachable from the server itself, and your team reaches Metabase through nginx on 443. The JAR can move to another port with MB_JETTY_PORT.
What if I lock myself out with the firewall?
You still get in: a rack server or blade keeps a separate console you open from the browser, showing the screen and the power button after SSH stops answering. Small cartridge servers mostly lack it; the m510 and m710x models are the exceptions, and the rest give you a power switch and nothing more.
How soon can I start?
A ready server is handed over within 30 minutes of payment approval; the payment and fraud checks before that are the part that varies. It is month to month, so you can move to a bigger machine if your dashboards outgrow it.

Give your dashboards a server of their own

Choose a machine with the memory Metabase and PostgreSQL need, and keep the whole of it to yourself.

Get a server for Metabase

Read next