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 healthcheck block ensures the Metabase container waits to start until PostgreSQL is fully ready to accept connections.
  • Port Binding: Metabase binds to 127.0.0.1:3000 rather than 0.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 -Xmx2g parameter 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 -