Two databases, two jobs
Metabase keeps its own state (users, saved questions, dashboards, collections, permissions, the connection details of your data sources) in the application database. The data you chart lives elsewhere, in what the docs call the data warehouse. Metabase reads the app DB settings only at startup, so it cannot be switched while the process runs.
| App DB option | Metabase docs status |
|---|---|
| PostgreSQL 14 or newer | Recommended for production |
| MySQL 8.4.0+ or MariaDB 10.6.0+ | Also works for production; utf8mb4 required |
| H2 (embedded file) | Default for local demos; avoid in production |
On Docker the H2 file sits inside the container, and the docs warn that removing the container loses it. If a team has already built dashboards on H2, the load-from-h2 command copies them into an empty PostgreSQL database, run with the same Metabase version that last wrote the H2 file. Do that before upgrading, not during.
Memory, Java and the machine
Metabase is a Java application. For the JAR, the docs name Java 25 as the supported runtime and recommend the Eclipse Temurin build. By default the JVM reserves about one fourth of the machine's RAM and does not hand it back, so an 8 GB box shows 2 GB in use whatever Metabase is doing.
- Raise the heap with
-Xmx, orJAVA_OPTS=-Xmx4gfor the container, when the heap graph shows a sawtooth of constant garbage collection or the logs report the heap running out. - The docs' rule: leave 1 to 2 GB for everything else, so
-Xmx1gon a 2 GB machine,2gon 4 GB, and so on. - On this layout PostgreSQL shares the machine. Our suggestion, not Metabase's: size the heap after giving the app DB its own room, and keep a large warehouse on a separate host.
A whole machine suits this because the JVM sizes itself from the RAM it sees, and on a dedicated server no other tenant's processes share that RAM, the cores or the disks, and you hold root. A single-socket machine is the usual base for an application with its database beside it; a dual-socket box carries Metabase next to a busy warehouse. If PostgreSQL itself is the reporting source, the PostgreSQL page covers tuning it; for event-scale analytics, Metabase connects to ClickHouse as a data source.
Firewall, secrets and containers
Run this as root (sudo -i) on Ubuntu with Docker Engine 28 or newer; docker version shows it, and Docker's own apt repository carries current releases. The firewall goes on first, with IPv6 rules enabled before any allow rule, because ufw writes IPv6 copies only for rules added while IPV6=yes. The two env files hold the passwords and keys at mode 600; Docker reads them literally, so values carry no quotes and no spaces. Writing them with heredocs keeps the secrets out of ps and out of shell history; the block sets umask 077 only while it writes them and puts it back to 022 afterwards.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw --force enable
umask 077
mkdir -p /opt/metabase && cd /opt/metabase
DBPASS=$(openssl rand -hex 24)
cat > pg.env <<EOF
POSTGRES_USER=metabase
POSTGRES_DB=metabaseappdb
POSTGRES_PASSWORD=$DBPASS
EOF
cat > metabase.env <<EOF
MB_DB_TYPE=postgres
MB_DB_HOST=mb-postgres
MB_DB_PORT=5432
MB_DB_DBNAME=metabaseappdb
MB_DB_USER=metabase
MB_DB_PASS=$DBPASS
MB_ENCRYPTION_SECRET_KEY=$(openssl rand -base64 32)
MB_SESSION_SECRET_KEY=$(openssl rand -base64 32)
MB_SITE_URL=https://bi.example.com
EOF
chmod 600 pg.env metabase.env
unset DBPASS
umask 022
docker network create metabase-net
docker run -d --name mb-postgres --restart unless-stopped \
--network metabase-net --env-file /opt/metabase/pg.env \
-v mb-pgdata:/var/lib/postgresql/data postgres:16
docker run -d --name metabase --restart unless-stopped \
--network metabase-net --env-file /opt/metabase/metabase.env \
-p 127.0.0.1:3000:3000 metabase/metabase:latest
docker logs -f metabase
A -p 3000:3000 mapping with no address publishes on every host address, 0.0.0.0 and [::]. Published IPv4 traffic skips ufw's rules, and IPv6 does too when Docker's own IPv6 networking is on (otherwise the [::] listener is docker-proxy, which ufw filters). Binding to 127.0.0.1 keeps port 3000 local, and Engine 28+ is what makes a 127.0.0.1 binding truly local-only (moby#45610). PostgreSQL publishes no port at all. Pin a release tag instead of latest once you know which version you run.
MB_ENCRYPTION_SECRET_KEY makes Metabase encrypt the connection details of each data source with AES256 + SHA512 before writing them to the app DB. Without that key they cannot be decrypted; lose or change it and every encrypted connection has to be re-entered in the Admin Panel. Setting it on a fresh install is all that is needed; an existing instance needs the one-time enable-encryption command first. The security guide also asks for MB_SESSION_SECRET_KEY on releases that support it. The JAR works the same way as a systemd service reading an EnvironmentFile; note that the JAR listens on localhost by default, the Docker image on 0.0.0.0.
The first admin, before anyone else
The account created in the setup wizard is an admin by default, and it adds users, connects databases and sets up email. Whoever reaches the wizard first gets that account, so finish it while nothing but the machine itself can reach port 3000.
- From your workstation, open a tunnel with your own login:
ssh -L 3001:127.0.0.1:3000 you@server. - Browse to
http://localhost:3001, click through the wizard and create the admin with a strong password. - Add your first data source with a dedicated read-only database user limited to the schemas Metabase needs, as the security guide recommends; or skip it and use the Sample Database.
- Only then publish the instance through the proxy below.
nginx and HTTPS in front
The security guide says to keep Metabase's port off the internet behind a reverse proxy you control, to set MB_SITE_URL to the https address, and to have the proxy send X-Forwarded-Proto. Point a DNS record at the server, then:
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/metabase <<'EOF'
server {
listen 80;
listen [::]:80;
server_name bi.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
ln -s /etc/nginx/sites-available/metabase /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d bi.example.com
Once HTTPS answers, the guide's MB_REDIRECT_ALL_REQUESTS_TO_HTTPS=true can go into metabase.env; recreate the container with the same docker run line for any env change. Open Source sign-in hardening from the same guide: MB_PASSWORD_COMPLEXITY=strong-enough, few admins, and deactivated accounts for people who leave.
Backups and upgrades
Metabase keeps all of its runtime state in that one SQL database, so a dump of it is the backup. Create the target directory first (mkdir -m 700 /var/backups/metabase), then add this file as /etc/cron.d/metabase-backup:
15 2 * * * root umask 077; docker exec mb-postgres pg_dump -U metabase -Fc metabaseappdb > /var/backups/metabase/app-$(date +\%F).dump
Copy the dumps off the machine yourself, and keep a copy of metabase.env somewhere separate: a dump restored without its encryption key comes back with unreadable connection details. Test a restore into a scratch database with pg_restore, as the security guide advises.
- Take a fresh dump (run the cron command by hand). The upgrade guide's first step is backing up the app DB. Note the running version from the startup log (
docker logs metabase), then keep the current image under its own name:docker tag metabase/metabase:latest metabase/metabase:previous. A pull replaceslatest, so without that tag nothing older is left to go back to. docker pull metabase/metabase:latest, thendocker rm -f metabase. The upgrade guide recommends pulling a specific release tag rather thanlatest; if you pin one, note it from the IMAGE column ofdocker ps --filter name=metabasebefore upgrading and you have your rollback target already.- Start it with the same
docker runline. Metabase migrates the app DB schema on startup. - To roll back:
docker rm -f metabase, then recreate the app DB empty, because tables the newer migrations added would otherwise survive the restore:docker exec mb-postgres dropdb -U metabase metabaseappdb && docker exec mb-postgres createdb -U metabase metabaseappdb. Restore into it withcat /var/backups/metabase/app-2026-10-23.dump | docker exec -i mb-postgres pg_restore -U metabase -d metabaseappdb(your file's date), then start the samedocker runline withmetabase/metabase:previous(or the earlier pinned tag) as the image. Starting the new image on the restored dump would only migrate it forward again.migrate down, run with the newer version, is the fallback when changes made after the upgrade must be kept. - From a release older than 40, step through each major version to the latest 40 first.
Licence: AGPL or commercial
Metabase's licence page states: "The Metabase Open Source Edition is offered under the AGPL." The image above, metabase/metabase, is that edition. Pro and Enterprise run a separate build, metabase/metabase-enterprise or the Enterprise JAR, and in the licence page's words: "Metabase Enterprise Edition binaries are released under our Metabase Commercial Software License." Their features stay off until a licence token is activated, under Admin > Settings > License or through an environment variable before start.
Embedding Metabase iframes in your own product has its own terms: the licence page lists abiding by the AGPL, the Embedding License (keeping the "Powered by Metabase" logo) or buying the commercial licence. Changing the code itself means abiding by the AGPL.
When this is the wrong setup
- Nobody will own upgrades, dumps and key custody. Self-hosting means running all three.
- You plan several Metabase nodes: they need a shared app DB, the same key pair on every node, and a scale-down to one node for each upgrade.
- Your warehouse is very large and busy: give it its own machine and point Metabase at it with a read-only user.
Questions
How much RAM should a Metabase server have?
Metabase's docs give a heap rule rather than a user count: the JVM takes about a quarter of RAM by default, and you raise it with -Xmx while leaving 1 to 2 GB for other processes. On a box that also runs PostgreSQL, leave room for the app DB as well. Watch heap use over time; a sawtooth of constant garbage collection means it needs more.
Is the built-in H2 database good enough to start with?
For a local trial, yes; for anything people depend on, no. The docs call H2 the default for local demos and say to avoid it in production, recommending PostgreSQL. Moving later is possible with load-from-h2, but it must use the same Metabase version that last wrote the H2 file.
Docker image or JAR file?
The docs recommend Docker for self-hosting. The JAR needs Java 25 on the host and runs as a systemd service with an environment file. Both read the same MB_ variables, so the app DB setup is identical.
What does MB_ENCRYPTION_SECRET_KEY protect?
The connection details of your data sources stored in the app DB, encrypted with AES256 + SHA512. Anyone who copies the app DB without the key cannot read them. Lose or change the key and those connections must be re-entered, so back it up apart from the dumps.
Do I need a commercial licence to self-host Metabase?
Not for the Open Source edition, which is offered under the AGPL. Pro and Enterprise features need the Enterprise build and an activated licence token under the Metabase Commercial Software License. Embedding Metabase in your own application brings its own licence terms.
How do I get console access if the firewall locks me out?
Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), so a bad ufw rule is fixed from the console rather than a ticket. Among cartridges, the m510 and m710x carry a console as well; the rest offer power control alone.