Metabase is an open-source business intelligence (BI) tool that lets you explore data, build dashboards, and share reports from a web browser. Teams often self-host it because it connects to common databases such as PostgreSQL, MySQL, MariaDB, and SQL Server. This guide deploys Metabase on a Linux server using Docker Compose with a PostgreSQL backend and Traefik providing automatic HTTPS, covering directory and environment setup, deployment, and the initial administrator setup through the web interface. By the end, you'll have Metabase running securely at your domain, ready to connect data sources and build dashboards.
Set Up the Project Directory and Environment Variables
Metabase stores its application data, such as users, dashboards, and saved questions, in PostgreSQL. This section creates the project directory and the environment file that the Compose stack shares.
1. Create the project directory and move into it:
$ mkdir ~/metabase
$ cd ~/metabase
2. Generate a random key that Metabase uses to encrypt stored data-source credentials:
$ openssl rand -base64 32
Save the output for use in the environment file.
3. Create the environment file:
$ nano .env
Add the following configuration. Replace YOUR_DB_PASSWORD with a strong password, YOUR_ENCRYPTION_KEY with the output from the previous openssl command, metabase.example.com with your domain name, and ADMIN_EMAIL with a deliverable email address for Let's Encrypt renewal notices.
POSTGRES_DB=metabaseappdb
POSTGRES_USER=metabase
POSTGRES_PASSWORD=YOUR_DB_PASSWORD
MB_ENCRYPTION_SECRET_KEY=YOUR_ENCRYPTION_KEY
MB_SITE_URL=https://metabase.example.com
DOMAIN=metabase.example.com
ACME_EMAIL=ADMIN_EMAIL
Save and close the file.
Deploy with Docker Compose
The deployment stack consists of Traefik for reverse proxy and certificate management, plus the Metabase and PostgreSQL containers on a shared bridge network. This configuration uses the official Metabase container image version v0.50.21.
1. Create the Docker Compose manifest:
$ nano docker-compose.yml
services:
postgres:
image: postgres:16
container_name: metabase-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
mem_limit: 1g
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- metabase-network
metabase:
image: metabase/metabase:v0.50.21
container_name: metabase
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: ${POSTGRES_DB}
MB_DB_PORT: 5432
MB_DB_USER: ${POSTGRES_USER}
MB_DB_PASS: ${POSTGRES_PASSWORD}
MB_DB_HOST: postgres
MB_ENCRYPTION_SECRET_KEY: ${MB_ENCRYPTION_SECRET_KEY}
MB_SITE_URL: ${MB_SITE_URL}
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
mem_limit: 2g
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- metabase-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.metabase.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.metabase.entrypoints=websecure"
- "traefik.http.routers.metabase.tls.certresolver=letsencrypt"
- "traefik.http.services.metabase.loadbalancer.server.port=3000"
traefik:
image: traefik:v3.7.10
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- metabase-network
volumes:
postgres-data:
networks:
metabase-network:
driver: bridge
Save and close the file. postgres runs PostgreSQL 16 as the database storing Metabase's users, dashboards, and saved questions. metabase runs the application server, starting only after PostgreSQL reports healthy, with Traefik labels registering it for automatic HTTPS on the configured DOMAIN. traefik handles reverse proxying and TLS termination on ports 80 and 443. mem_limit and logging cap each container's memory and rotate its logs, preventing either from exhausting the server's resources.
2. Launch the containers:
$ docker compose up -d
3. Verify that the services are running:
$ docker compose ps -a
The output displays all three containers in an Up state, with a healthy status shown for metabase-postgres and metabase once their health checks pass.
4. Query the Metabase health endpoint through your domain:
$ curl https://metabase.example.com/api/health
Your output should be similar to the one below:
{"status":"ok"}
5. View the service logs to confirm Metabase loaded the configuration successfully:
$ docker compose logs -f metabase
The output displays PostgreSQL accepting connections and Metabase completing its startup migrations, which take one to two minutes. Press Ctrl+C to stop following the logs once the service reports that it has started.
Access Metabase and Complete the Initial Setup
Open Metabase in a browser and complete the first-run wizard.
- Open your domain over HTTPS in a browser. Replace
metabase.example.comwith your domain:
https://metabase.example.com
- Select your preferred language.
- Create the initial administrator account by entering your name, email address, and password.
- When prompted to add your first data source, configure one now or skip and add it later.
Setup is now complete, and Metabase is ready to use. Any databases you connect through the interface stay separate from the PostgreSQL container, which stores only Metabase's own metadata.
Next Steps
Metabase is live with Traefik-managed HTTPS and a PostgreSQL-backed metadata store. From here you can:
- Connect your production databases and start building dashboards and saved questions
- Configure user groups and permissions to control who can see which data
- Set up scheduled email or Slack reports (pulses) so dashboards reach stakeholders automatically
For the full guide with additional tips, visit the original article on Vultr Docs.
Top comments (1)
I like that this guide doesn't stop at getting Metabase running. The health checks, memory limits, log rotation, and the separation between Metabase's application data and the databases it connects to are the details that make a deployment much more practical.
The PostgreSQL health check before starting Metabase is a small thing, but it saves you from a lot of confusing startup problems. Same with exposing only Traefik on 80/443 and letting it handle the HTTPS side.
I've seen plenty of Docker guides where
docker compose up -dis basically the end of the story. Here, checking the health endpoint and logs afterwards makes the deployment feel much closer to something you'd actually want to maintain.One thing I'd be interested in seeing in a follow-up is backup and restore for the PostgreSQL metadata volume. Getting the dashboard online is one thing, but being able to recover the Metabase state is where self-hosting gets really interesting.