Analytics & Intelligence

Metabase Docker Traefik SSL Setup: Step-by-Step Guide

J
James Eriksson
··9 min read
Deploy Metabase with Docker and Traefik SSL. Docker-compose setup with Let's Encrypt and PostgreSQL. Learn why most teams choose managed hosting instead.
TL;DR
  • Deploy Metabase on Docker with Traefik reverse proxy and Let's Encrypt SSL in 45 minutes using docker-compose.
  • PostgreSQL provides persistent data storage and avoids H2 database limitations for production Metabase deployments.
  • Subpath routing does not work in Metabase; deploy at the root domain instead of a subdirectory.
  • Self-hosting costs $20-50 monthly for compute but adds 4-6 hours per month of operational overhead for patches, backups, and certificate monitoring.
  • Managed Metabase hosting eliminates DevOps complexity and provides EU data residency without self-managed infrastructure.

You can run Metabase in production with Docker, Traefik, and SSL in about an hour. The setup uses Let's Encrypt for free certificates, PostgreSQL for persistent data, and Traefik as a reverse proxy that handles HTTPS termination. But operational reality: certificate renewal, security patches, database backups, and Traefik updates demand ongoing attention. This guide walks through the complete docker-compose configuration and shows you exactly what self-hosting costs.

What This Architecture Does

This setup gives you three things: Metabase runs inside a Docker container, Traefik handles all HTTPS traffic and routes it to Metabase on port 3000, and Let's Encrypt issues SSL certificates automatically (renewed every 90 days by Traefik). PostgreSQL runs in its own container and persists your Metabase metadata (dashboards, queries, user accounts) to a volume. No vendor lock-in. No managed platform fees. Everything runs on your own infrastructure.

The trade-off is that you own the operational debt. Certificate renewal is automatic with Traefik, but you must monitor it. Database backups are your responsibility. Security patches for Metabase, PostgreSQL, Traefik, and the Linux host are your patches to apply. At scale, this is why most teams migrate to Opsily's managed Metabase hosting. If you want to skip the DevOps overhead, managed hosting eliminates this entire operational surface.

Prerequisites You'll Need

Before starting, have these in place: Docker and Docker Compose installed (version 1.29+ for compose file version 3.8 support), a domain name with DNS A record pointing to your server's public IP, a server or VPS with at least 2 vCPU and 4 GB RAM (Metabase uses 1-2 GB depending on query load), and basic familiarity with docker-compose and networking.

If you are running Metabase on a home server or behind a NAT, Traefik cannot reach Let's Encrypt, so you will need to use a DNS challenge or run this on a public IP address. Let's Encrypt's ACME protocol requires external HTTPS verification.

For production, you should also set up an SMTP server or email service for Metabase user invitations. This is optional but recommended so users can self-register and reset passwords.

Step 1: Create a Docker Network for Container Communication

Start by creating an isolated Docker network that Traefik and Metabase will use to talk to each other:

docker network create proxy

Why: Docker containers on the default bridge network cannot resolve each other by hostname. By creating an explicit "proxy" network, Traefik can connect to Metabase using the container name "metabase" as a hostname. This is also cleaner for security: your proxy network is isolated from other containers.

Step 2: Configure PostgreSQL for Metabase

Metabase's default H2 database is sufficient for testing but fails under production load. Use PostgreSQL instead. Here is the service definition for your docker-compose.yml:

services:
  postgres:
    image: postgres:15-alpine
    container_name: metabase-db
    environment:
      POSTGRES_USER: metabase
      POSTGRES_PASSWORD: your-secure-password-here
      POSTGRES_DB: metabase
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - proxy
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U metabase"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Key points: The postgres:15-alpine image is small and production-ready. The healthcheck block tells Traefik and Metabase when the database is actually ready (not just when the container starts). The postgres_data volume persists your database to the host filesystem, so your dashboards and queries survive container restarts. Never use environment variables for sensitive passwords in production--use Docker secrets or an external secret manager, but for this guide we will keep it simple.

Step 3: Configure Traefik for SSL and Let's Encrypt

Traefik acts as your reverse proxy. It listens on ports 80 and 443, issues SSL certificates from Let's Encrypt, and routes HTTPS traffic to Metabase. Add this service to docker-compose.yml:

  traefik:
    image: traefik:v3.1
    container_name: traefik
    command:
      - "--api.insecure=false"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      -./letsencrypt:/letsencrypt
    networks:
      - proxy
    restart: unless-stopped

What this does: Traefik listens on port 80 (HTTP) and redirects all traffic to port 443 (HTTPS). The certificatesresolvers block configures Let's Encrypt. Replace your-email@example.com with an email Let's Encrypt can use to notify you of certificate expiration (though Traefik handles renewal automatically).

The /var/run/docker.sock volume gives Traefik permission to inspect running containers and their labels. The ./letsencrypt directory on your host stores the acme.json file (your issued certificates). Back this up: if you lose acme.json, you lose your certificates and must wait 7 days before reissuing.

Step 4: Deploy Metabase Behind Traefik

Now add Metabase. This is where Traefik's routing rules live in container labels:

  metabase:
    image: metabase/metabase:latest
    container_name: metabase
    environment:
      MB_DB_TYPE: postgres
      MB_DB_DBNAME: metabase
      MB_DB_HOST: postgres
      MB_DB_PORT: 5432
      MB_DB_USER: metabase
      MB_DB_PASS: your-secure-password-here
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.metabase.rule=Host(`your-domain.com`)"
      - "traefik.http.routers.metabase.entrypoints=websecure"
      - "traefik.http.routers.metabase.tls.certresolver=letsencrypt"
      - "traefik.http.services.metabase.loadbalancer.server.port=3000"
    networks:
      - proxy
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

Critical points: The MB_DB_* environment variables tell Metabase how to connect to PostgreSQL. MB_DB_HOST: postgres uses Docker's container name resolution (which works because both are on the "proxy" network). The labels are Traefik's routing rules. Replace your-domain.com with your actual domain. traefik.http.services.metabase.loadbalancer.server.port=3000 tells Traefik that Metabase listens on port 3000 inside the container. Do not expose port 3000 to the host (no ports section for Metabase). Traefik is the only entry point. depends_on with service_healthy ensures PostgreSQL is ready before Metabase starts. Without this, Metabase fails to connect.

Step 5: Launch and Verify Everything Works

Create a file called docker-compose.yml with all the services above (postgres, traefik, metabase). Put this in a directory on your server and run:

docker compose up -d

Check the logs:

docker compose logs -f metabase

You should see "Metabase initialization complete" after about 30 seconds. Then navigate to your domain using HTTPS in your browser (e.g., if your domain is analytics.example.com, prepend https:// to reach it).

If you get a certificate error, Traefik is still waiting for Let's Encrypt to issue the cert (takes 30-60 seconds). Refresh after a minute.

You will see the Metabase setup wizard. Create your admin account and configure your first database connection. Your dashboards and queries are now persisted to PostgreSQL. Restart the containers and your work is still there.

Common Pitfalls and How to Avoid Them

Metabase does not load assets (CSS, JavaScript fail to load). This usually means your browser loaded the page over HTTP (not HTTPS), so mixed-content policy blocks the assets. Verify Traefik is issuing the certificate by checking docker compose logs traefik. If the certificate is not issued, wait 60 seconds and refresh.

Subpath routing does not work. You cannot run Metabase at a subdirectory of your domain (e.g., yourdomain.com/reporting or yourdomain.com/analytics). Metabase's application assumes it runs at the root of the domain. A user in the Traefik community forum documented this issue and eventually switched to Grafana. If you need to run multiple tools at the same domain, use separate subdomains (analytics.yourdomain.com, reporting.yourdomain.com) instead.

PostgreSQL connection timeout. If Metabase fails to start with "connection refused" for postgres:5432, check that the healthcheck in the postgres service is passing. Run docker compose ps and confirm postgres shows a healthy state. If it is not, run docker compose logs postgres to see why.

Database lock errors on startup. If Metabase crashes with "database is locked," Traefik's acme.json might be corrupted. This happens when you kill the containers during Traefik startup. Traefik was writing to acme.json when you killed it. Solution: stop everything, delete acme.json, and restart.

Let's Encrypt rate limits. If you destroy and recreate Traefik more than 5 times per hour with the same domain, Let's Encrypt rate-limits you. The rate limit is 50 certificates per domain per week. In production, be deliberate about restarts.

The Real Cost of Self-Hosting

Compute: A 2-vCPU, 4 GB RAM VPS costs $20-50 per month depending on your provider. Hetzner Cloud is at the lower end; DigitalOcean is mid-range. Linode and AWS are higher.

Storage: PostgreSQL with 10,000 dashboard queries and saved questions takes about 2-5 GB. Most VPS plans include at least 50 GB, so this is not a bottleneck yet.

Bandwidth: Metabase queries use minimal egress. If you have 50 users running 20 queries a day, that is maybe 100 MB of egress per month. Most VPS plans have 1-10 TB included.

Hidden costs: Certificate renewal monitoring (you should alert if Traefik fails to renew), security patches (monthly, 30 minutes per patch cycle), PostgreSQL backups (weekly, requires automation and off-site storage), Traefik version updates (quarterly, test before deploying to production). Aggregate: 4-6 hours per month.

For most teams, this is why managed Metabase hosting becomes attractive. Managed hosting gives you automated backups, security patching, EU data residency, and no Traefik configuration. You pay a monthly fee instead of hourly attention.

Frequently Asked Questions

Does Traefik need access to the Docker socket?

Yes. Traefik reads /var/run/docker.sock to inspect running containers and their labels. This is safe in production if you run Traefik in its own container (not on the host). Never expose docker.sock over the network.

Can I use Nginx instead of Traefik?

Yes, but Nginx requires manual Let's Encrypt certificate management. Traefik renews automatically. Nginx is simpler if you have one or two services; Traefik is worth it if you have many.

What if I need to run Metabase on a subdirectory like /analytics?

Metabase does not support this. The application code assumes it runs at the root domain. Running it at a subdirectory path like yourdomain.com/analytics will break asset loading and navigation. Use subdomains instead (analytics.yourdomain.com).

How do I back up my Metabase data?

Back up the PostgreSQL database using pg_dump inside the container: docker compose exec postgres pg_dump -U metabase metabase > backup.sql. Store the backup off-site (S3, cloud storage, another server). Automate this weekly.

Can I use Docker Compose instead of Kubernetes?

Yes. Kubernetes is overkill for Metabase unless you have 500+ concurrent users. Docker Compose is sufficient for up to 100 users.

Should I use managed hosting or self-host?

Self-host if you have DevOps expertise and want to minimize costs. Use Opsily's managed Metabase solution if you value your time and want EU data residency. Managed hosting includes automated backups, security patches, and no certificate drama.

How do I scale Metabase to multiple containers?

Add multiple Metabase service definitions in docker-compose.yml and configure Traefik's loadbalancer to round-robin between them. This gets complex fast; managed hosting handles scaling for you.

The Bottom Line

You can deploy Metabase behind Traefik with Let's Encrypt SSL in 45 minutes. Docker-compose handles networking, PostgreSQL provides durable storage, and Traefik renews certificates automatically. But self-hosting demands ongoing operational work: certificate monitoring, security patches, backups, and Traefik updates. For most teams running Metabase in production, that overhead is why managed Metabase hosting makes sense.

Skip the DevOps work
Opsily manages Metabase on EU infrastructure with automated backups, security patches, and zero certificate drama.
Get Started Free

Ready to self-host your own apps?

One server. Multiple apps. No per-app fees.

Get started →