Self-Hosting Guide: Deploy Metabase for Analytics 2026
Metabase is the most popular open source BI tool — it lets non-technical users explore data, build dashboards, and share insights without writing SQL. Self-hosting gives you unlimited users and dashboards for free.
Requirements
- VPS with 2 GB RAM minimum (4 GB recommended)
- Docker and Docker Compose
- Domain name (e.g.,
bi.yourdomain.com) - 10+ GB disk
- A database to analyze (PostgreSQL, MySQL, etc.)
Step 1: Create Docker Compose
# docker-compose.yml
services:
metabase:
image: metabase/metabase:latest
container_name: metabase
restart: unless-stopped
ports:
- "3000:3000"
environment:
- MB_DB_TYPE=postgres
- MB_DB_DBNAME=metabase
- MB_DB_PORT=5432
- MB_DB_USER=metabase
- MB_DB_PASS=your-strong-password
- MB_DB_HOST=db
- MB_SITE_URL=https://bi.yourdomain.com
- MB_ENCRYPTION_SECRET_KEY=your-random-32-char-key
depends_on:
- db
db:
image: postgres:16-alpine
container_name: metabase-db
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=metabase
- POSTGRES_USER=metabase
- POSTGRES_PASSWORD=your-strong-password
volumes:
postgres_data:
Note: This PostgreSQL is for Metabase's own data (dashboards, users, settings). Your business data lives in separate databases that you'll connect in Step 6.
Step 2: Generate Encryption Key
openssl rand -hex 16
This encrypts database credentials stored in Metabase.
Step 3: Start Metabase
docker compose up -d
First boot takes 1-2 minutes to initialize.
Step 4: Reverse Proxy (Caddy)
# /etc/caddy/Caddyfile
bi.yourdomain.com {
reverse_proxy localhost:3000
}
sudo systemctl restart caddy
Step 5: Initial Setup Wizard
- Open
https://bi.yourdomain.com - Select your language
- Create admin account
- Connect your first database (can skip and add later)
- Choose usage tracking preference
Step 6: Connect Your Data Sources
Go to Admin → Databases → Add database:
| Database | Connection String |
|---|---|
| PostgreSQL | host:5432/dbname |
| MySQL | host:3306/dbname |
| MongoDB | mongodb://host:27017/dbname |
| SQLite | /path/to/database.db |
| BigQuery | Service account JSON |
| Snowflake | Account + credentials |
| Redshift | host:5439/dbname |
Tip: Create a read-only database user for Metabase:
CREATE USER metabase_reader WITH PASSWORD 'read-only-password';
GRANT CONNECT ON DATABASE myapp 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;
Step 7: Build Your First Dashboard
Create a question (query):
- Click New → Question
- Pick your database and table
- Use the visual query builder or write SQL
- Save the question
Build a dashboard:
- Click New → Dashboard
- Add saved questions as cards
- Add filters (date range, category, etc.)
- Arrange and resize cards
- Save and share
Common dashboard patterns:
| Dashboard | Metrics |
|---|---|
| Revenue | MRR, churn rate, LTV, new subscriptions |
| Product | DAU/MAU, feature usage, retention cohorts |
| Support | Ticket volume, response time, CSAT scores |
| Marketing | Traffic, conversion rate, CAC, channel performance |
Step 8: Configure SMTP for Alerts
Admin → Settings → Email:
| Setting | Value |
|---|---|
| SMTP Host | smtp.resend.com |
| SMTP Port | 587 |
| SMTP Security | TLS |
| SMTP Username | resend |
| SMTP Password | re_your_api_key |
| From Address | bi@yourdomain.com |
Enables:
- Scheduled dashboard emails (daily/weekly reports)
- Alert notifications (when metrics cross thresholds)
- User invitation emails
Step 9: Set Up Alerts
- Open a question/chart
- Click the bell icon → Create alert
- Choose condition: "When results go above/below X"
- Set recipients and frequency
- Alerts fire automatically when conditions are met
Step 10: Embedding (Optional)
Embed Metabase dashboards in your app:
// Generate signed embed URL (server-side)
const jwt = require('jsonwebtoken');
const METABASE_SITE_URL = 'https://bi.yourdomain.com';
const METABASE_SECRET_KEY = 'your-embedding-secret-key';
const payload = {
resource: { dashboard: 1 },
params: {},
exp: Math.round(Date.now() / 1000) + (10 * 60), // 10 min expiration
};
const token = jwt.sign(payload, METABASE_SECRET_KEY);
const embedUrl = `${METABASE_SITE_URL}/embed/dashboard/${token}`;
<iframe
src="https://bi.yourdomain.com/embed/dashboard/TOKEN"
width="100%"
height="600"
frameborder="0"
></iframe>
Enable embedding in Admin → Settings → Embedding.
Production Hardening
Environment tuning:
environment:
- JAVA_OPTS=-Xmx2g # Increase for large datasets
- MB_JETTY_MAXTHREADS=100
- MB_ASYNC_QUERY_THREAD_POOL_SIZE=10
Backups:
# Database backup (daily cron)
docker exec metabase-db pg_dump -U metabase metabase > /backups/metabase-$(date +%Y%m%d).sql
Updates:
docker compose pull
docker compose up -d
Security:
- Use read-only database users
- Enable SSL for all database connections
- Restrict Admin access to specific users
- Set session timeout in Admin settings
Resource Usage
| Users | RAM | CPU | Disk |
|---|---|---|---|
| 1-10 | 2 GB | 2 cores | 10 GB |
| 10-50 | 4 GB | 4 cores | 15 GB |
| 50-200 | 8 GB | 8 cores | 30 GB |
VPS Recommendations
| Provider | Spec (20 users) | Price |
|---|---|---|
| Hetzner | 4 vCPU, 8 GB RAM | €8/month |
| DigitalOcean | 2 vCPU, 4 GB RAM | $24/month |
| Linode | 2 vCPU, 4 GB RAM | $24/month |
Why Self-Host Metabase
Metabase Cloud's pricing scales aggressively with seat count. The Starter plan is $85/month for up to 5 users — reasonable for a small team. But the Pro plan, which adds SSO, advanced permissions, and sandboxing, starts at $500/month. If your data team grows to 20+ people who need BI access, managed Metabase costs $1,000–3,000/month depending on features required. Tableau and Looker charge even more: Tableau Creator licenses run $75/user/month, and Looker starts at $5,000/month for its entry tier.
Self-hosted Metabase Open Source is completely free — unlimited users, unlimited dashboards, unlimited data sources. You pay only for the server: a Hetzner CX31 at €6.49/month comfortably handles a 20-person analytics team. The annual savings against Metabase Cloud Pro (20 users) exceed $12,000 per year. For startups with limited budget but real analytics needs, this is a significant advantage.
Full data access control: In SaaS BI tools, your data queries leave your infrastructure. For companies handling sensitive business data — revenue figures, user PII, customer contracts — having query results pass through a vendor's servers creates compliance risk. Self-hosted Metabase queries your databases directly from within your own infrastructure; nothing routes through Metabase Labs' servers.
Embedding without vendor lock-in: Metabase's signed embedding feature is exceptionally useful for SaaS products that want to offer customers analytics dashboards. The embedding feature is fully available in the open source version — something that competitors like Looker and Tableau charge substantial premiums for as an "embedded analytics" product.
When NOT to self-host Metabase: Metabase is a Java application with a non-trivial resource footprint. It requires 2 GB RAM minimum, and the startup time is 1–2 minutes from cold. If your data team needs advanced features like custom models (formerly DBT integration), advanced caching, or enterprise SSO, some of these require Metabase Pro or Enterprise. Evaluate whether the open source feature set meets your requirements before committing to self-hosting.
Prerequisites (Expanded)
VPS with 2 GB RAM minimum (4 GB recommended): Metabase runs on a JVM and has a real memory floor. With 2 GB total, you're splitting memory between Metabase (which needs 1.5 GB for JVM heap) and PostgreSQL (which handles the application's own metadata). Under memory pressure, Metabase becomes slow and may crash mid-query. Budget 4 GB for any deployment with more than 5 concurrent users.
Docker and Docker Compose: The Compose file above deploys Metabase and its dedicated PostgreSQL instance together. The PostgreSQL in this Compose file stores Metabase's own data (dashboards, saved questions, user accounts, permissions). Your business databases that you want to analyze are separate — you'll connect them in Step 6 using Metabase's database connection UI.
10+ GB disk: Metabase's PostgreSQL database stays small (a few hundred MB even for complex deployments), but query result caching and Metabase's internal analytics can consume additional space over time. 10 GB provides comfortable headroom. Monitor disk usage with df -h and set up alerts if it approaches 80%.
A database to analyze: Metabase's value comes from connecting to your existing data sources. If you're running PostgreSQL or MySQL for your application, you can create a read-only user (see Step 6) and point Metabase at it. Start with GRANT SELECT ON ALL TABLES and narrow permissions as you understand what Metabase needs.
For VPS selection, the JVM heap requirement pushes Metabase into a higher tier than most self-hosted tools. See the VPS comparison for self-hosters for a breakdown of providers at the 4–8 GB RAM tier, where Hetzner offers the best price-to-performance ratio.
Production Security Hardening
Metabase has privileged access to your business databases — it can read (and potentially write, if you misconfigure permissions) all your data. Its security posture matters.
Firewall with UFW: Port 3000 is Metabase's internal port. Only expose 80, 443, and 22 publicly.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Use read-only database connections: As shown in Step 6, create a dedicated metabase_reader user with SELECT-only permissions on the databases Metabase analyzes. Never connect Metabase with superuser or application-write credentials. This contains the blast radius if Metabase is compromised or a user runs a destructive query.
Fail2ban for SSH:
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Keep secrets in environment variables: MB_DB_PASS and MB_ENCRYPTION_SECRET_KEY must not be committed to version control. Store them in a .env file excluded from Git. If using Docker Compose without a separate .env file, be careful never to docker compose config output in a public terminal session — it prints all resolved environment variables including secrets.
Disable SSH password authentication:
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd
Enable SSL for database connections in Metabase: When adding databases in Admin → Databases, check "Use a secure connection" to enforce TLS between Metabase and your data sources, especially if they're on separate servers.
Automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Back up the Metabase PostgreSQL database regularly: This database stores all your dashboards, saved questions, collections, and user permissions. Losing it means rebuilding your entire BI setup. Use automated server backups with restic to automate daily PostgreSQL dumps with off-site copies. For a complete server hardening guide, see the self-hosting security checklist.
Troubleshooting Common Issues
Metabase takes 5+ minutes to start
Metabase runs database migrations on first start and after each upgrade. The first boot is always slow — 2–5 minutes is normal. If subsequent starts are consistently slow, your PostgreSQL container may be competing for resources. Check container resource usage with docker stats. If the JVM is hitting memory limits, increase JAVA_OPTS=-Xmx2g to allocate more heap.
"Connection refused" when adding a database
Metabase runs in a Docker container and uses container networking. When connecting to a database running in another container, use the container name or Docker network alias, not localhost. For a PostgreSQL container named app-db on the same Docker network, the host is app-db, not 127.0.0.1. For databases on external servers, use the server IP or hostname directly.
Dashboards loading very slowly
Slow dashboards usually indicate slow underlying queries. Click the clock icon on any card in the dashboard to see query execution time. Use Metabase's built-in query caching (Admin → Settings → Caching) to cache expensive query results — set cache TTL based on how frequently your data changes. For dashboards with many cards, stagger the cache expiry times to prevent all queries from running simultaneously.
Users can't see certain tables
Metabase's data permissions are configured in Admin → Permissions → Data. Check that the user's group has access to the schema and table in question. Note that permissions cascade: access to a database doesn't automatically grant access to all schemas within it. Also verify that the database user Metabase uses (metabase_reader) has SELECT permissions on the specific table — GRANT SELECT ON ALL TABLES IN SCHEMA public doesn't cover tables created after the grant was issued unless you also ran ALTER DEFAULT PRIVILEGES.
Scheduled reports not being sent
If scheduled dashboard emails aren't arriving, start by testing the SMTP configuration in Admin → Settings → Email → Test. Check the Metabase logs for email errors:
docker compose logs metabase | grep -i "mail\|smtp\|email" | tail -20
Also verify that the scheduled report is assigned to a user with a valid email address and that the user has permission to view all cards on the dashboard.
Embedding returns 403 or shows blank iframe
JWT embedding requires that the token's exp (expiration) claim is in the future, the signing key matches what's configured in Admin → Settings → Embedding, and the resource.dashboard ID is a valid dashboard ID. Generate a fresh token for each page load — don't cache JWT tokens. Also check that the MB_SITE_URL environment variable matches exactly the URL you're embedding from, including protocol.
Set up automated server backups with restic to protect your Metabase dashboards and database connections.
Compare BI tools on OSSAlt — features, data sources, and self-hosting options side by side.
See open source alternatives to Metabase on OSSAlt.