DEV Community

Amritasha Agrawal
Amritasha Agrawal

Posted on

Self-Hosting Metabase on Railway: The App Database, JVM Memory and the Encryption Key You Can't Add Later

Metabase is the open-source BI tool people install when they want dashboards over their own database without writing a frontend. The open-source edition is free to self-host, and it's a single Java process — which makes it look like the easiest thing on your list to deploy.

It mostly is. But four decisions get made for you by default, and each one is painful to change after your team has built dashboards on it: where Metabase stores its own data, how much memory the JVM may use, whether connection credentials are encrypted, and how upgrades happen.

This guide runs Metabase on Railway, a usage-billed container platform with managed Postgres and private networking, so there's no VM to look after. Most of it applies to any container host.

Two databases, not one

Metabase talks to two kinds of database, and it's worth keeping them separate in your head:

What it is What's in it
Application database Metabase's own storage Every question, dashboard, collection, user, permission, and the connection details for your data sources
Data sources The databases you analyse Your actual data — Metabase only reads it (unless you enable writes)

Losing a data source connection is an inconvenience. Losing the application database is losing every dashboard anyone ever built.

Decision 1: Don't run on H2

If you start the Metabase image with no configuration, it stores the application database in an embedded H2 file inside the container. Metabase's own docs say not to run production on H2, and on a container platform it's worse than "not recommended": the container filesystem is replaced on every redeploy, so an H2 file that isn't on a volume disappears with your dashboards in it.

Point Metabase at Postgres from the first boot:

MB_DB_TYPE=postgres
MB_DB_HOST=<postgres host>
MB_DB_PORT=5432
MB_DB_DBNAME=<database>
MB_DB_USER=<user>
MB_DB_PASS=<password>
Enter fullscreen mode Exit fullscreen mode

On Railway, set these as reference variables pointing at the Postgres service, so they track the database automatically and stay on the private network.

If you already have dashboards on H2, Metabase has a load-from-h2 migration command — run it once, before you have two diverging copies.

Decision 2: Set the encryption key on day one

MB_ENCRYPTION_SECRET_KEY makes Metabase encrypt the connection details it stores for your data sources — hostnames, usernames and passwords for your production databases. Without it, those sit in the application database in plaintext.

# at least 16 characters; Metabase's docs generate one like this:
openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

The catch is timing. On a fresh Metabase, setting the key is all you need. On a Metabase that already has data sources, Metabase won't start with the key set until you've run the one-off enable-encryption CLI command against the application database. And if you ever lose or change the key, every encrypted connection has to be re-entered by hand in the admin panel (there's a rotate-encryption-key command for planned rotation).

So: generate it before the first boot, put it in your secret store, and never edit it casually.

Decision 3: Tell the JVM how much memory it gets

Metabase runs on the JVM. A container-aware JVM sizes its maximum heap from the container's memory limit — by default a quarter of it. On a platform where the limit is a generous plan ceiling rather than what you intend to pay for, that default is arbitrary: it can be too small for a busy instance, and it's not a deliberate cost ceiling either.

The Metabase image appends whatever you put in JAVA_OPTS, so set the heap explicitly:

JAVA_OPTS=-Xmx2g
Enter fullscreen mode Exit fullscreen mode

Two things to know about out-of-memory behaviour: the official image starts Java with -XX:+CrashOnOutOfMemoryError, so a heap overflow kills the process rather than leaving it half-alive, and your platform's restart policy decides what happens next. If that policy gives up after a few retries, Metabase stays down — worth knowing before the first big dashboard query hits it.

Decision 4: Pin the version, back up before upgrading

Metabase migrates its application database on startup when you move to a newer version. Their upgrade guide is blunt about two things:

  1. Back up the application database before every upgrade. Downgrading after a major upgrade means restoring that backup (or running a migrate down command from the newer version's JAR).
  2. Don't let the orchestrator kill Metabase mid-migration, or you can end up with a corrupted application database.

On a container platform that translates to:

  • Pin an exact image tag (metabase/metabase:v0.xx.y), never latest, so an unrelated redeploy can't upgrade you.
  • Give the health check (/api/health) a long timeout — the template below uses 600 seconds — so a slow migration isn't mistaken for a crash.
  • Take a backup, then bump the tag, then watch the logs until migrations finish.

Connecting your data

Metabase's own recommendation is a dedicated database user with read-only access to the schemas you want to analyse. It's the cheapest safety net you'll ever set up: a dashboard can't drop a table if the account can't write.

Two more things that affect the database you connect:

  • Sync and scans run on their own. By default Metabase does a lightweight hourly schema sync and a heavier daily scan of field values. On a large production database, move them to off-peak hours in the database's admin settings.
  • Point it at a replica if you have one. Analytical queries and transactional traffic compete for the same resources.

If the data source is another Railway service in the same project, connect over the private network (<service>.railway.internal) rather than the public TCP proxy — it's faster and avoids egress charges.

Email and alerts on Railway

Dashboard subscriptions and alerts can go to email or Slack. Email uses SMTP, and Railway blocks outbound SMTP on the Free, Trial and Hobby plans — so on those plans email subscriptions won't be delivered. Slack subscriptions go over Slack's HTTPS API and aren't affected; set up the Slack app under Admin → Settings → Slack.

Also set MB_SITE_URL to your public URL, so links in subscriptions and alerts point somewhere real.

Deploying it

I maintain a Railway template that sets up the application-database side for you: a pinned Metabase image (bump the tag in the Dockerfile to upgrade), Railway Postgres 18 as the application database wired in with reference variables, and a 600-second health-check timeout for first-boot and upgrade migrations.

Deploy on Railway

(Disclosure: I earn a share of Railway usage from deployments of this template. The source is public at Amritasha/metabase-railway, and everything above applies if you set it up yourself.)

After the deploy goes green:

  1. Before opening the URL, add MB_ENCRYPTION_SECRET_KEY and JAVA_OPTS to the Metabase service and redeploy. Doing it now avoids the enable-encryption step later.
  2. Open the public URL and complete the setup wizard — it creates the admin account. Do this straight away; until it's done, anyone with the URL can claim the instance.
  3. Set MB_SITE_URL to the public URL.
  4. Create a read-only database user and add your data source with it.

Backups

The only stateful thing is the application database:

  • Railway volume backups. The Postgres service's volume has a Backups tab with daily (kept 6 days), weekly (kept 27 days) and monthly (kept 89 days) schedules. Turn it on.
  • One copy off-platform. Railway backups restore only within the same project and environment. A periodic pg_dump covers the rest:
  pg_dump "$DATABASE_PUBLIC_URL" -Fc -f metabase-$(date +%F).dump
Enter fullscreen mode Exit fullscreen mode

(Use a client at least as new as the server — 18 for the template.)

  • Back up your encryption key separately. A restored database is useless if its connection details are encrypted with a key you no longer have.

What it costs

Railway bills per second: about $10 per GB of RAM per month, $20 per vCPU per month, and $0.16 per GB-month of volume storage. Hobby is $5/month including $5 of usage; Pro is $20/month including $20.

Metabase is a long-running JVM, so it holds its memory whether or not anyone is looking at a dashboard, and its RAM is most of the bill; the Postgres application database is small. Your -Xmx sets the ceiling for the heap, and the JVM uses some memory beyond it. After a week, the Metabase service's Metrics tab shows what your team's usage actually costs.

When not to self-host it

  • You need SAML/JWT single sign-on, row-level data permissions or official support. Those are in Metabase's paid editions.
  • Nobody will own upgrades. A self-hosted BI tool that's never upgraded accumulates security fixes it doesn't have.
  • Your data lives somewhere Metabase can't reach privately. Exposing a production database to the public internet so a dashboard can read it is a worse trade than paying for hosting near the data.

Checklist

  • [ ] Application database is Postgres, not H2
  • [ ] MB_ENCRYPTION_SECRET_KEY set before the first data source, and stored safely
  • [ ] JAVA_OPTS=-Xmx... set deliberately
  • [ ] Image pinned to an exact version; long health-check timeout
  • [ ] Setup wizard completed right after deploy
  • [ ] MB_SITE_URL set
  • [ ] Read-only database user for each data source
  • [ ] Sync/scan schedule moved off-peak for large databases
  • [ ] Email on a plan with SMTP, or Slack for subscriptions
  • [ ] Volume backups + off-platform pg_dump; encryption key backed up separately

If something here is wrong or out of date, say so in the comments — I'll fix it.

Top comments (0)