Skip to Content

Production Guide: Deploy Docmost with Docker Compose, Traefik, and PostgreSQL on Ubuntu

A practical, operations-first deployment with security controls, backup strategy, and day-2 troubleshooting.

If your team needs privacy-respecting product analytics without handing user data to a third party, self-hosting Docmost is a strong option. The common failure mode is not installationβ€”it is weak production hygiene after installation. Teams skip secret isolation, fail to define backup routines, and treat reverse-proxy TLS as an afterthought. This guide focuses on the production path from day one: controlled configuration, explicit security boundaries, and verification steps that prevent silent breakage after updates.

We will deploy Docmost with Docker Compose, Traefik, and PostgreSQL on Ubuntu. The pattern is built for small teams and internal platforms that need a maintainable stack: reproducible infrastructure-as-code, predictable upgrade windows, and enough observability to troubleshoot incidents quickly. If you have ever inherited a "works on my VM" setup with no rollback path, this runbook is designed to avoid that outcome.

Architecture and request flow overview

The stack has three layers: Traefik as entry point and TLS terminator, application containers for the web and worker processes, and PostgreSQL as the state layer. External traffic lands on Traefik over 443, is routed by host rule to the application service on the internal Docker network, and then reads/writes telemetry data to PostgreSQL.

We keep all services on a private bridge network and expose only Traefik ports publicly. That boundary simplifies firewall policy and reduces accidental service exposure. Persistent volumes are mounted only where state is required (database and app data), making backups deliberate and restore drills testable.

# Expected traffic path
# Client -> Traefik (443) -> Umami app (3000) -> PostgreSQL (5432 internal)

docker network create analytics_net || true
docker network inspect analytics_net --format '{{.Name}} {{len .Containers}} containers'

If the copy button does not work in your browser/editor, manually select and copy the command block.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with at least 2 vCPU, 4 GB RAM, and 40 GB SSD.
  • DNS A record for your analytics domain (for example, analytics.example.com).
  • Sudo-capable shell account and basic familiarity with Docker logs.
  • Open inbound ports 22, 80, and 443 at the firewall/security group layer.
  • A maintenance window for first deploy and a second short window for validation after DNS/TLS.

Step-by-step deployment

1) Install Docker Engine and Compose plugin

sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg lsb-release jq

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg |   sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

If the copy button does not work in your browser/editor, manually select and copy the command block.

2) Create project directory and strict file permissions

sudo mkdir -p /opt/umami/{traefik,database,backups}
sudo chown -R $USER:$USER /opt/umami
chmod 750 /opt/umami
cd /opt/umami

If the copy button does not work in your browser/editor, manually select and copy the command block.

3) Generate secrets and write environment file

cd /opt/umami
POSTGRES_PASSWORD=$(openssl rand -base64 36 | tr -d '
')
APP_SECRET=$(openssl rand -hex 32)

cat > .env <<EOF
DOMAIN=analytics.example.com
TZ=UTC
POSTGRES_DB=umami
POSTGRES_USER=umami
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
APP_SECRET=${APP_SECRET}
EOF

chmod 600 .env

If the copy button does not work in your browser/editor, manually select and copy the command block.

4) Create Docker Compose stack

cat > /opt/umami/docker-compose.yml <<'YAML'
services:
  traefik:
    image: traefik:v3.0
    container_name: umami-traefik
    restart: unless-stopped
    command:
      - --api.dashboard=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - [email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.letsencrypt.acme.httpchallenge=true
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/letsencrypt
    networks: [analytics_net]

  db:
    image: postgres:16-alpine
    container_name: umami-db
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./database:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 12
    networks: [analytics_net]

  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami-app
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      APP_SECRET: ${APP_SECRET}
    depends_on:
      db:
        condition: service_healthy
    labels:
      - traefik.enable=true
      - traefik.http.routers.umami.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.umami.entrypoints=websecure
      - traefik.http.routers.umami.tls=true
      - traefik.http.routers.umami.tls.certresolver=letsencrypt
      - traefik.http.services.umami.loadbalancer.server.port=3000
    networks: [analytics_net]

networks:
  analytics_net:
    external: true
YAML

touch /opt/umami/traefik/acme.json
chmod 600 /opt/umami/traefik/acme.json

If the copy button does not work in your browser/editor, manually select and copy the command block.

5) Launch and validate first boot

cd /opt/umami
docker compose pull
docker compose up -d

docker compose ps
docker logs --tail=80 umami-app
docker logs --tail=80 umami-db

If the copy button does not work in your browser/editor, manually select and copy the command block.

6) Apply host hardening and baseline firewall policy

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verbose

If the copy button does not work in your browser/editor, manually select and copy the command block.

Configuration and secrets handling best practices

Keep .env local to the server with mode 600. Never paste credentials into tickets, chat, or wiki pages. If multiple operators manage the stack, move secret distribution to a vault-backed workflow and render environment files just-in-time during deploy. Avoid editing secrets directly inside docker-compose.yml; this prevents accidental commits and makes rotation explicit.

For rotation, change one secret class at a time (database password, then app secret) and run a smoke test after each change. Coupling all secret changes in one window is a common source of ambiguous failures. Maintain a lightweight runbook entry for each rotation: date, operator, impacted services, and rollback instruction. This is boring work, but it is exactly what shortens incident recovery when authentication breaks at 2 AM.

# Example: rotate only APP_SECRET safely
cd /opt/umami
cp .env .env.bak.$(date +%F-%H%M)
sed -i "s/^APP_SECRET=.*/APP_SECRET=$(openssl rand -hex 32)/" .env
docker compose up -d umami
docker logs --tail=120 umami-app

If the copy button does not work in your browser/editor, manually select and copy the command block.

Verification checklist

Run these checks before announcing the service as production-ready. You are looking for deterministic signals: healthy containers, reachable HTTPS endpoint, successful database connectivity, and no crash-loop in logs.

cd /opt/umami

docker compose ps
curl -I https://analytics.example.com

docker exec umami-db pg_isready -U umami -d umami

docker logs --tail=120 umami-app | egrep -i 'error|exception|migrate|ready'
docker logs --tail=120 umami-traefik | tail -n 40

If the copy button does not work in your browser/editor, manually select and copy the command block.

If these checks pass, complete one application smoke path in the UI: sign in, load dashboard, create a test website property, then remove it. Functional smoke testing catches errors that basic liveness checks miss, especially after image updates.

Common issues and fixes

Traefik serves 404 for the analytics hostname

Usually the router host rule does not match your DNS name, or the container labels were not reloaded. Confirm DOMAIN in .env, inspect labels with docker inspect umami-app, then restart app and proxy in sequence. Also verify that DNS record points to the same host where Traefik is running.

Container starts but app shows database connection errors

Check that DATABASE_URL references service name db (not localhost), and confirm the password in .env matches PostgreSQL container environment. If credentials changed, run controlled restart: database first, then app.

Let's Encrypt certificate is not issued

ACME HTTP challenge requires public reachability on port 80. If your cloud firewall blocks 80 or traffic is behind another reverse proxy, certificate issuance can fail silently. Open port 80 temporarily for challenge completion and ensure no parallel proxy consumes the same port.

High memory usage after traffic spikes

Set explicit container memory reservations/limits and observe behavior over a week. Some teams over-correct with aggressive limits that trigger OOM restarts. Tune incrementally and watch restart count plus p95 response time rather than relying on one-off snapshots.

FAQ

Can I run this behind Cloudflare?

Yes. Start direct first for baseline validation, then place Cloudflare in front once HTTPS and origin behavior are stable. Keep SSL mode set to Full (strict) and avoid masking origin misconfiguration with permissive modes.

How often should I back up PostgreSQL?

At minimum daily for low-volume setups; more frequently if analytics data is critical for reporting cadence. Keep at least one off-host copy and test restore monthly. A backup strategy without restore drills is only documentation.

What is the safest way to update images?

Pin versions, capture pre-upgrade backup, then pull and restart during a maintenance window. Validate with the checklist in this guide and keep rollback commands ready. Avoid unattended auto-updaters on business-critical stacks.

Do I need a separate staging environment?

If analytics is used for customer-facing decisions, yes. A minimal staging copy catches migration and proxy-rule mistakes before production impact. Even a small single-node staging host is better than testing directly in production.

How should I secure admin access?

Use unique admin credentials, restrict SSH with keys only, and keep host patching current. If your identity stack allows it, place admin access behind SSO and MFA. Rotate credentials on a schedule and on every operator offboarding event.

Can I use managed PostgreSQL instead of containerized PostgreSQL?

Absolutely. Many teams do this to reduce database operations burden. Update DATABASE_URL to managed endpoint, enforce TLS parameters where required, and review egress/firewall rules before cutover.

What metrics should I alert on first?

Start with service availability, TLS expiry window, container restart counts, host disk utilization, and database health checks. These baseline alerts provide high signal with low noise and catch most early operational regressions.

Related guides

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Grafana Loki with Docker Compose + Traefik + S3 on Ubuntu
A production-focused Loki deployment with TLS ingress, durable object storage, retention controls, and operational validation.