Power BI is Microsoft-only and not self-hostable. Tableau costs $70+ per seat per month. Looker became Looker Studio became something Google can deprecate anytime. Metabase is the open-source BI tool with the best out-of-the-box experience — connect to any database, build dashboards in minutes, share with your team for zero per-seat cost. The self-hosted version has every feature of the paid Cloud version.
This guide installs Metabase via Docker with a PostgreSQL backend (instead of the H2 default — H2 dies under any real load), sets up HTTPS via Nginx, connects multiple data sources, walks through building a real dashboard, and configures email alerts and shareable dashboards. By the end you'll have what most analytics teams pay $5,000–$50,000/year for, running on a $15.99/month VPS.
- A Linux VPS — Ubuntu 22.04 or Debian 12, 4 GB RAM minimum
- A domain pointed at your VPS
- One or more databases you want to analyse (their connection details)
- Roughly 25 minutes
For a Metabase VPS pre-tuned with Postgres backend and JVM heap sized correctly, see our Metabase VPS plans.
1. The H2 trap — read this before installing
Metabase's default install uses an embedded H2 database for its application data (dashboard definitions, user accounts, saved questions — not your analytics data, but Metabase's own metadata). H2 is fine for development. H2 will corrupt itself under real load and you will lose all your dashboards.
This is documented in Metabase's own docs but easy to miss. Every self-hoster who treats Metabase casually loses everything they built about 6 months in, when the H2 file finally gives up.
Use Postgres for Metabase's app DB from day one. This guide does it correctly.
2. Prepare the VPS
apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg ufw
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
adduser metabase
usermod -aG sudo metabase
rsync --archive --chown=metabase:metabase ~/.ssh /home/metabase
3. Install Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker metabase
4. Deploy Metabase with Postgres backend
su - metabase
mkdir -p ~/metabase/pgdata
cd ~/metabase
DB_PW=$(openssl rand -base64 24)
echo "POSTGRES_PASSWORD=$DB_PW" > .env
chmod 600 .env
Create compose.yaml:
services:
postgres:
image: postgres:16-alpine
container_name: mb-postgres
restart: unless-stopped
volumes:
- ./pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: metabase
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: metabase_app_db
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabase"]
interval: 5s
retries: 10
metabase:
image: metabase/metabase:latest
container_name: metabase
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase_app_db
MB_DB_PORT: 5432
MB_DB_USER: metabase
MB_DB_PASS: ${POSTGRES_PASSWORD}
MB_DB_HOST: postgres
# JVM tuning — give Metabase real heap
JAVA_OPTS: "-Xmx2g -XX:+UseG1GC"
# Timezone — affects scheduled jobs and alerts
MB_QP_CACHE_BACKEND: "db"
JAVA_TIMEZONE: "UTC"
Start it:
docker compose up -d
docker compose logs -f metabase
First boot is slow — Metabase's JVM takes 60–90 seconds to start, and it runs database migrations against the Postgres app DB on the way up. Watch for Metabase Initialization COMPLETE in the logs. Once you see that, it's ready.
5. Nginx reverse proxy + HTTPS
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/metabase <<'EOF'
server {
listen 80;
server_name bi.example.com;
# Metabase queries can take a while
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/metabase /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d bi.example.com
6. First-run setup
Visit https://bi.example.com. Metabase's setup wizard:
- Language
- Admin account (email + password)
- "What will you use Metabase for?" — pick whatever; doesn't matter
- "Add your data" — you can skip this here and add data sources later
- "Usage data preferences" — opt out if you prefer
Click Take me to Metabase. You land on the home page with a "Started from scratch" panel.
7. Connect your first data source
The whole point. Settings (gear icon, top right) → Admin Settings → Databases → + Add database.
Metabase has native drivers for ~25 databases. Pick yours:
- PostgreSQL — most common
- MySQL / MariaDB
- MS SQL Server
- BigQuery / Redshift / Snowflake — cloud warehouses
- MongoDB / DynamoDB — NoSQL
- ClickHouse / Druid — analytics-specific
- SQLite — yes, even SQLite
For Postgres (most common):
- Display name: production-db
- Host: the Postgres IP/hostname your VPS can reach
- Port: 5432
- Database name: prod
- Username: a read-only user (recommended — limit Metabase's access)
- Password
- Use a secure connection (SSL): yes if your DB is remote
Important: create a read-only user for Metabase. Don't connect with your DB superuser. In Postgres:
CREATE USER metabase_reader WITH PASSWORD 'strong-password';
GRANT CONNECT ON DATABASE prod TO metabase_reader;
GRANT USAGE ON SCHEMA public TO metabase_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO metabase_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO metabase_reader;
Save in Metabase. It scans your schema (takes a few seconds for medium DBs, minutes for huge ones) and surfaces tables in the data browser.
8. Build your first dashboard
Click + New (top nav) → Question. Pick a table, then pick a summary type:
- Count of rows by date — automatic line chart
- Sum of column grouped by another column — bar chart
- Single number / KPI — big number tile
Metabase guesses the right visualization. Tweak via the visualization picker. Save the question.
Now create a dashboard: + New → Dashboard. Drag your saved questions onto the canvas, resize, arrange. Add filters (date range, status, etc.) that auto-apply across all questions on the dashboard.
For SQL power users: + New → SQL query. Write raw SQL, parameterise with {{var}} syntax, visualize results. Combined with native + visual questions, you get the best of both worlds.
🐾 Metabase pre-tuned
Our Metabase VPS plans ship with Postgres app DB pre-configured, JVM heap sized for the plan tier, and HTTPS ready. From $7.99/mo, deploy in 5 minutes.
See Metabase Plans →9. Sharing, alerts, and subscriptions
Three ways to share insights:
Permission-based sharing — give users accounts (Free for unlimited users, that's the point) and set per-collection permissions. Most teams operate this way.
Public links — for a single question or dashboard, click Sharing → Enable sharing. Get a tokenised URL that anyone can view without an account. Toggle off when you're done. Useful for one-off external sharing.
Embedded analytics — embed a Metabase dashboard inside your own product. Static embedding (signed JWT URLs) is free; interactive embedding (with full filtering) requires the paid Pro tier. Most self-hosters use static embedding.
Email subscriptions — set a dashboard to email itself to a list daily/weekly. Configure SMTP under Admin Settings → Email; Mailgun, Postmark, SES all work.
Slack subscriptions — Metabase can post dashboard images to Slack on a schedule. Set up the Slack integration under Admin Settings → Slack.
Alerts — for a single question, set an alert: "when this metric crosses 100, email me". Useful for ops monitoring — failed-payment rate, signup conversion, support queue length.
10. Reference: scaling, embedding, alternatives
Resource needs by usage
| Plan | Concurrent users | Notes |
|---|---|---|
| Starter ($7.99, 4 GB) | 5–10 | JVM wants 2 GB heap; tight on Starter. Pro recommended. |
| Pro ($15.99, 8 GB) | 20–50 | Sweet spot for small/mid teams |
| Premium ($35.99, 8 GB) | 100+ | Plus separate Postgres tier for larger app DB |
JVM tuning
Metabase is a JVM app. The JAVA_OPTS: "-Xmx2g" in our compose file sets the heap to 2 GB. Bump to -Xmx4g on the Pro plan and -Xmx6g on Premium. Leave at least 25% of RAM for the OS and Postgres. Setting heap too high causes long GC pauses and slow page loads.
Query result caching
Metabase can cache query results to reduce load on your production DB. Admin Settings → Caching → enable. Set per-dashboard or per-question TTL. Tables that don't change much (last week's data) → cache for hours. Realtime metrics → don't cache.
Embedded analytics — making it look like yours
White-label the embedded view by setting your own logo + colors under Admin Settings → Appearance. Embedded dashboards inherit these styles. To remove Metabase branding entirely, the paid Pro tier is required.
Metabase vs Apache Superset
Superset is more powerful (richer chart types, SQL Lab IDE, dbt integration) but harder to operate (Python + Celery + Redis stack, frequent upgrades break things). Metabase is more polished out of the box and easier for non-technical users. Pick Superset if you're a data team that wants maximum control; pick Metabase if you want most of the value with a tenth of the ops burden.
Backups
Two things to back up: the Postgres app DB (dashboards, questions, users) and your data warehouse (your actual business data — handled separately). Daily mysqldump-equivalent for the Metabase app DB is plenty. Use our Postgres backup guide for the actual data.
FAQ
Why pay for Metabase Cloud at $85/user/month when self-hosting is free?
You don't have to. The self-hosted version has every feature of Cloud except the managed-hosting convenience. Cloud is appropriate if you don't want to maintain a server and the per-user cost makes sense for your team size. Self-hosted Metabase on a $15.99 VPS supports unlimited users — break-even is at one user.
Can I connect to a database on another server?
Yes — Metabase connects over the network. Open the source DB's port to your Metabase VPS IP (firewall rule). For added security, route through a SSH tunnel or a private WireGuard network so the DB never exposes a public port. See our WireGuard tutorial for the tunnel approach.
Will Metabase queries slow down my production database?
Potentially — analytics queries can be heavy. Best practice: connect Metabase to a read replica (Postgres streaming replication, MySQL read replica) rather than the primary. Replication lag is typically under a second; analytics queries don't usually need second-by-second freshness. Or use the built-in query result caching to reduce repeated queries.
Does Metabase support row-level security?
Yes — via Sandboxes. Configure a sandboxed table per user role: for example, sales reps see only their own region's data, support sees only their own tickets. Configured per-collection or per-table. Powerful but takes some setup.
Can I write my own SQL?
Yes — Metabase has a SQL editor with autocomplete, parameter syntax, schema browsing. Native questions are stored alongside GUI questions and behave identically (same caching, same dashboards, same sharing). Set users to 'no SQL' role if you want them GUI-only — native queries are gated by role permission.
What about real-time data?
Metabase polls; it doesn't subscribe. Auto-refresh dashboards every N seconds is supported (down to ~10 seconds). For true real-time, your data source needs to support sub-second updates and you'll be polling Metabase aggressively. Most teams settle for 5-minute or hourly refreshes — true real-time is rarely worth the load.