Metabase Business Intelligence: Deploy a Data Analytics Dashboard in Docker
Learn how to set up Metabase using Docker Compose. Query your database, generate SQL visual charts, and build data dashboards on your VPS.
Deploying Metabase on a Virtual Private Server (VPS) via Docker Compose provides a robust, isolated environment for business intelligence and data analytics. While Metabase ships with an embedded H2 database for quick evaluation, a production deployment requires a dedicated, persistent application database like PostgreSQL.
This guide provides the complete configuration files, security practices, and deployment steps to run Metabase in production.
System Prerequisites
Before starting, ensure your VPS meets the following minimum requirements: * Operating System: Linux (Ubuntu 22.04 LTS or Debian 12 recommended) * Resources: Minimum 2 GB RAM (4 GB recommended if handling large datasets or schema syncs) and 2 vCPUs. * Docker Engine: Version 20.10+ * Docker Compose: Version 2.0+ * DNS: A registered domain name or subdomain (e.g., metabase.example.com) pointed to your VPS IP address.
Directory Structure
Organize your configuration files under /opt/metabase. This structure isolates container persistent data, certificates, and backup scripts.
/opt/metabase/
├── docker-compose.yml
├── .env
├── nginx.conf
└── backups/
Docker Compose Configuration
Create a docker-compose.yml file to define two services: Metabase and PostgreSQL (the metadata application database).
Create the directory and navigate into it:
sudo mkdir -p /opt/metabase
cd /opt/metabase
Create the docker-compose.yml file:
version: '3.8'
services:
metabase-db:
image: postgres:15-alpine
container_name: metabase_db
restart: always
environment:
POSTGRES_DB: metabaseappdb
POSTGRES_USER: metabaseadmin
POSTGRES_PASSWORD: ${METABASE_DB_PASSWORD}
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabaseadmin -d metabaseappdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- metabase-network
metabase:
image: metabase/metabase:v0.49.3
container_name: metabase_app
restart: always
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabaseappdb
MB_DB_PORT: 5432
MB_DB_USER: metabaseadmin
MB_DB_PASS: ${METABASE_DB_PASSWORD}
MB_DB_HOST: metabase-db
MB_JETTY_PORT: 3000
# Limit JVM max heap size to prevent OOM termination (adjust according to VPS RAM)
JAVA_OPTS: "-Xmx2g -XX:+UseG1GC"
ports:
- "127.0.0.1:3000:3000"
depends_on:
metabase-db:
condition: service_healthy
networks:
- metabase-network
networks:
metabase-network:
driver: bridge
Key Configuration Directives:
- Healthchecks: Metabase relies heavily on its backend database during startup. The
healthcheckblock ensures the Metabase container waits to start until PostgreSQL is fully ready to accept connections. - Port Binding: Metabase binds to
127.0.0.1:3000rather than0.0.0.0:3000. This prevents external access directly to the container port, forcing all traffic through the reverse proxy. - JVM Tuning (
JAVA_OPTS): The-Xmx2gparameter allocates a maximum heap size of 2 GB to the Java Virtual Machine. This is crucial for avoiding random out-of-memory crashes on VPS environments during heavy data query operations.
Environment Secret File
Create a .env file in the same directory to store the database password:
METABASE_DB_PASSWORD=generate_a_long_secure_password_here
Secure the .env file permissions so only root or authorized users can read it:
sudo chmod 600 .env
Reverse Proxy Configuration
Nginx acts as the entry point, handling incoming requests, terminating SSL certificates, and forwarding traffic to Metabase.
Step 1: Install Nginx
sudo apt update
sudo apt install nginx -y
Step 2: Configure the Site Block
Create an Nginx configuration file at /etc/nginx/sites-available/metabase.conf:
server {
listen 80;
server_name metabase.example.com;
# Redirect all HTTP requests to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name metabase.example.com;
# SSL configuration (Paths managed by Certbot)
ssl_certificate /etc/letsencrypt/live/metabase.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/metabase.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "no-referrer-when-downgrade";
# Support large file uploads (e.g., CSV imports)
client_max_body_size 15M;
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;
# Timeouts adjusted for heavy queries and dashboard loading
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
send_timeout 600s;
}
}
Enable the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/metabase.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 3: SSL Certificate via Certbot
To obtain Let's Encrypt certificates automatically, use Certbot:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d metabase.example.com
Certbot will automatically verify the domain ownership, retrieve the SSL certificates, and inject the correct configuration paths into the Nginx configuration.
Deployment Execution
With configurations finalized, start the containers:
cd /opt/metabase
sudo docker compose up -d
Verify that both containers are active:
sudo docker compose ps
Monitor the logs to confirm the Metabase Java application has successfully initialized the database and started the Jetty webserver:
sudo docker compose logs -f metabase
Look for the following log output indicating readiness: Metabase Initialization COMPLETE
Maintenance: Automating Database Backups
Because all question queries, database metadata, user access roles, and dashboard structures reside in the PostgreSQL metadata database, scheduling regular backups is critical.
Create a backup script at /opt/metabase/backup.sh:
#!/bin/bash
BACKUP_DIR="/opt/metabase/backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DB_CONTAINER="metabase_db"
DB_USER="metabaseadmin"
DB_NAME="metabaseappdb"
# Ensure the backup directory exists
mkdir -p "$BACKUP_DIR"
# Execute pg_dump inside the database container
docker exec -t "$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" | gzip > "$BACKUP_DIR/metabase_backup_$TIMESTAMP.sql.gz"
# Set secure permissions
chmod 600 "$BACKUP_DIR/metabase_backup_$TIMESTAMP.sql.gz"
# Retain only the last 30 backups
find "$BACKUP_DIR" -type f -name "metabase_backup_*.sql.gz" -mtime +30 -delete
Make the script executable:
chmod +x /opt/metabase/backup.sh
Add a cron job to run the backup daily at 02:00 AM:
# Open crontab editor
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/metabase/backup.sh") | crontab -