Self-host Metabase with Postgres
Follow this on your own server. Start free, no card needed
Most BI tools charge per seat, which turns “let the whole team see the dashboard” into a line item that grows every time someone new needs access. Metabase is open-source self-serve analytics that does the core job, connect a database, build charts and dashboards, let non-technical people ask questions without writing SQL, without a per-seat bill once it’s running on your own server. This guide sets it up with its own Postgres database for storing dashboards and questions, behind Caddy for HTTPS, then shows the one-click path.
By the end you’ll have Metabase running at your own domain, connected to its own metadata store, ready to point at whatever database or warehouse you actually want to analyze.
What you’ll need
- A server. Metabase itself is a JVM application with modest requirements for a small team; a Hetzner CX23 (2 vCPU, 4 GB RAM, 40 GB NVMe) at €5.99/month excluding VAT runs Metabase and its own Postgres comfortably. Heavier dashboard usage or large result sets want more memory; a CX33 (4 vCPU, 8 GB, €8.99/month) is the next step up.
- A domain or subdomain with a DNS A record pointed at the server (this guide uses
bi.example.com). - SSH access and about 15 minutes.
- Optionally, the database or warehouse you actually want Metabase to query. This guide only sets up Metabase’s own application database; connecting your real data source happens from inside the app after first login.
Step 1: Provision the server and point DNS at it
bi.example.com. A 203.0.113.40
ssh root@203.0.113.40
apt update && apt upgrade -y
Step 2: Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker --version
docker compose version
Step 3: Write the environment file
mkdir -p /opt/metabase && cd /opt/metabase
DB_NAME=metabase
DB_USER=metabase
DB_PASSWORD=change-this-to-a-random-string
This password is for Metabase’s own Postgres database, the one holding dashboards, saved questions and user accounts, not whatever data source you’ll eventually connect it to.
Step 4: Write the Docker Compose file
This guide uses the current stable release, Metabase v0.64.3.4, pinned explicitly rather than latest so an unrelated upstream release can’t change your stack underneath you:
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
metabase:
image: metabase/metabase:v0.64.3.4
restart: unless-stopped
environment:
MB_DB_TYPE: postgres
MB_DB_HOST: db
MB_DB_PORT: "5432"
MB_DB_DBNAME: ${DB_NAME}
MB_DB_USER: ${DB_USER}
MB_DB_PASS: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 10
db:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- metabase-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
metabase-db-data:
caddy-data:
caddy-config:
MB_DB_TYPE, MB_DB_HOST, MB_DB_PORT, MB_DB_DBNAME, MB_DB_USER and MB_DB_PASS are the exact environment variables Metabase’s own docs specify for pointing it at an external Postgres database instead of the embedded H2 database it falls back to without them, which isn’t meant for anything beyond a quick trial. Validate the file:
docker compose config
Step 5: Write the Caddyfile
bi.example.com {
reverse_proxy metabase:3000
}
Step 6: Bring the stack up
docker compose up -d
docker compose ps
You should see caddy, metabase and db all running, with metabase reporting healthy once its own startup finishes, which takes a minute or two on first boot since it’s initializing its metadata schema in Postgres. Tail the logs if it’s taking longer:
docker compose logs -f metabase
Visit https://bi.example.com; you should land on Metabase’s first-run setup wizard over a valid HTTPS certificate.
Step 7: Finish setup and connect a real data source
The setup wizard creates your admin account directly, no default password to change afterward the way some self-hosted apps work. Once you’re in, add the database you actually want to analyze from Settings → Admin settings → Databases → Add a database. This is a separate connection from the Postgres container in Step 4, which only stores Metabase’s own questions and dashboards, not your data.
Step 8: Back up the metadata database
Dashboards, saved questions, and user accounts live in the Postgres container from Step 4, not in the data sources you connect. Back it up on a schedule:
docker compose exec -T db pg_dump -U ${DB_USER} ${DB_NAME} | gzip > /opt/metabase/backups/metabase-$(date +%F).sql.gz
Copy the file off the server on a cron job. Restoring it after a fresh install gets your dashboards and questions back; your actual data sources are unaffected either way since they’re not stored here.
Step 9: Upgrades
docker compose pull metabase
docker compose up -d metabase
Check Metabase’s own release notes before jumping across a major version number; it runs its own database migrations automatically on startup, and a backup from Step 8 beforehand is cheap insurance against one going wrong.
The shortcut: do it with Railyard
Everything above, the two containers, the environment file, the reverse proxy, is what Railyard’s Metabase + Postgres stack template sets up for you. Pick it from the templates catalog: https://app.railyard.run/deploy?stack=metabase-postgres. Railyard provisions Metabase and a dedicated Postgres instance together and wires the connection automatically, so there’s nothing to connect by hand. The same separation from Step 7 still applies: this Postgres instance is Metabase’s own metadata store, and your actual warehouse or production database gets connected separately from Settings → Databases after first login. From there, database backups run on a schedule and restore with one click.
Troubleshooting
HTTPS never issues. DNS isn’t pointing at the server yet. dig +short bi.example.com should return the server’s IP.
Metabase container restarts in a loop right after startup. Check docker compose logs metabase for a database connection error first; the most common cause is MB_DB_PASS not matching what’s actually in the db container’s environment, usually because .env wasn’t present or named correctly when the stack first came up.
First login takes a long time, or times out in the browser. Metabase runs its own schema migrations against Postgres on first boot, which can take a minute or two depending on server speed. Check docker compose logs metabase is still progressing rather than stuck before assuming something’s broken.
Dashboards are slow to load. This is more often the data source Metabase is querying than Metabase itself; large unindexed queries against a production database will be slow regardless of what’s asking. Check query performance against the actual connected database, and consider a read replica for heavy reporting load if the production database itself is under pressure.
Out of memory on a small server. The JVM Metabase runs on has real baseline memory needs; a CX23 is enough for a small team, but if you’re also running other containers on the same box, docker compose ps showing metabase as exited combined with dmesg | grep -i "out of memory" confirms it, and the fix is either more memory or fewer things sharing this server.
FAQ
Can I connect more than one database? Yes, any number, each configured separately from Settings → Databases; Metabase itself only needs the one Postgres instance from this guide for its own storage regardless of how many sources you connect to it.
Does Metabase need internet access to run? No, once installed it runs entirely against its own Postgres database and whatever data sources you connect, with no outbound dependency for day-to-day operation.
What’s the difference between the open-source and paid versions? The open-source edition covers dashboards, questions, alerts and the query builder this guide sets up; Metabase’s paid tiers add things like SSO, audit logs and white-labeling aimed at larger organizations, which this guide doesn’t need.
Can non-technical people actually use this without learning SQL? That’s the specific feature Metabase is built around: its question builder lets someone filter, group and chart data through a visual interface, with SQL available as an option for anyone who wants it rather than a requirement.
How is this cheaper than a hosted BI tool? A hosted per-seat tool’s cost grows with how many people in the company need a dashboard; a CX23 at roughly six euros a month is the same cost whether three people or thirty look at it.
Can I run Metabase without Docker? Yes, it also ships as a plain JAR file you run with a JVM directly, which some teams prefer on a server already managing Java deployments; the Docker image is the path this guide and Railyard’s template both use, since it keeps the JVM version and dependencies pinned alongside the app itself instead of depending on whatever’s already installed on the host.
Metabase gives a team self-serve dashboards without turning “one more person needs access” into a billing conversation. The setup here is two containers, an environment file and a reverse proxy; from there it’s the same question builder and dashboards, running on a server with a fixed monthly cost instead of one that scales with headcount.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.