Skip to Content

Production Guide: Deploy Leantime with Docker Compose + Caddy + MySQL on Ubuntu

Self-host an ADHD-friendly project management platform with full data ownership, automatic TLS, and a production-hardened MySQL backend

Most project management tools charge per seat, lock your data in their cloud, and add friction every time your team tries to adapt the workflow to how they actually think. Leantime is different: it is an open-source project management platform built with focus and accessibility in mind — including ADHD-friendly features like time-boxing, goal tracking, and distraction-reduced views. You own the database, you own the backups, and you pay nothing per seat.

This guide walks you through a production-ready Leantime deployment on Ubuntu using Docker Compose for container orchestration, Caddy as the TLS-terminating reverse proxy, and MySQL as the primary database. The result is a multi-user project workspace with automatic HTTPS, container health checks, and a repeatable upgrade path.

Architecture and flow overview

The stack is intentionally small. Three containers run behind a single Caddy reverse proxy hosted directly on the Ubuntu server:

  • leantime — the PHP application container listening on port 80 internally
  • db — MySQL 8.0 with a named volume for persistent data
  • caddy — Caddy 2 using network_mode host for direct port 443 binding and automatic Let's Encrypt certificate provisioning

Caddy terminates TLS from the public internet, forwards decrypted traffic to the leantime container on the Docker bridge network, and handles HTTP-to-HTTPS redirects automatically. The .env file is the single source of truth for all runtime secrets; the Compose file references it without hard-coding any credentials. Persistent data lives in Docker-managed named volumes so container rebuilds do not lose your projects or uploads.

Prerequisites

  • Ubuntu 22.04 or 24.04 server (2 vCPU, 2 GB RAM minimum; 4 GB recommended for teams)
  • Docker Engine 24+ and Docker Compose plugin installed (apt install docker.io docker-compose-plugin)
  • Caddy 2 installed as a system service (apt install caddy or official Caddy apt repo)
  • A DNS A record pointing your chosen subdomain (e.g. pm.yourdomain.com) to the server's public IP
  • UFW or equivalent firewall with ports 80 and 443 open inbound
  • Root or sudo access

Step-by-step deployment

1. Prepare the project directory

Create an isolated directory for Leantime. Restrict permissions so only root can read the secrets file you will create next:

mkdir -p /opt/leantime && cd /opt/leantime
chmod 700 /opt/leantime

2. Create the environment file

Generate a strong random secret for the application session key before writing the file:

openssl rand -hex 32   # use output as LEAN_SESSION_PASSWORD

Create /opt/leantime/.env:

# MySQL
MYSQL_ROOT_PASSWORD=changeme_root_password
MYSQL_DATABASE=leantime
MYSQL_USER=leantime
MYSQL_PASSWORD=changeme_db_password

# Leantime app
LEAN_DB_HOST=db
LEAN_DB_PORT=3306
LEAN_DB_DATABASE=leantime
LEAN_DB_USER=leantime
LEAN_DB_PASSWORD=changeme_db_password
LEAN_SESSION_PASSWORD=paste_openssl_output_here
LEAN_APP_URL=https://pm.yourdomain.com
LEAN_SITENAME=MyCompany PM
chmod 600 /opt/leantime/.env

3. Create the Docker Compose file

Create /opt/leantime/docker-compose.yml:

version: "3.9"

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    env_file: .env
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - leantime_db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 15s
      timeout: 5s
      retries: 5

  leantime:
    image: leantime/leantime:latest
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:8080:80"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - leantime_uploads:/var/www/html/userfiles
      - leantime_public_uploads:/var/www/html/public/userfiles

volumes:
  leantime_db:
  leantime_uploads:
  leantime_public_uploads:

4. Create the Caddyfile

Add a site block to /etc/caddy/Caddyfile. Caddy handles TLS automatically; no certificate management commands are required:

pm.yourdomain.com {
    reverse_proxy 127.0.0.1:8080
    encode gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
    log {
        output file /var/log/caddy/leantime.log
        format json
    }
}
mkdir -p /var/log/caddy && chown caddy:caddy /var/log/caddy
systemctl reload caddy

5. Start the stack and run initial setup

Pull images and start all containers in detached mode. Wait for the database health check to pass before the app container starts:

cd /opt/leantime
docker compose pull
docker compose up -d
docker compose logs -f leantime | grep -E "ready|error|listening|started"

Once the logs show the application is ready, navigate to https://pm.yourdomain.com in a browser to complete the first-time setup wizard. You will be prompted to create the initial admin account and organization name. This step writes the schema into MySQL — do not skip it.

Configuration and secrets handling

All runtime secrets live exclusively in /opt/leantime/.env. Never commit this file to a Git repository. Key practices for a production deployment:

  • Rotate LEAN_SESSION_PASSWORD periodically. After rotation, all active sessions are invalidated and users must log in again. Schedule this during a low-traffic window.
  • Use a dedicated MySQL user with minimal grants. The Compose file already creates one (leantime) that only has access to the leantime database. Do not use root for the application connection.
  • External secret management: For teams with a secrets manager (HashiCorp Vault, Infisical, AWS Secrets Manager), replace direct environment variable references with a pre-start script that writes a fresh .env from vault at container start. This avoids secrets at rest in the file system.
  • Email configuration: Add SMTP variables (LEAN_SMTP_HOST, LEAN_SMTP_PORT, LEAN_SMTP_USER, LEAN_SMTP_PASSWORD, LEAN_SMTP_SECURE) to .env and restart the stack to enable invitation emails and notifications.
  • File upload paths: The two userfiles volumes cover both server-side and public-side uploads. Back them up alongside the database or file uploads will be orphaned after a restore.

Verification

# 1. Check all containers are Up and healthy
docker compose ps

# 2. Confirm Caddy obtained a TLS certificate
curl -svI https://pm.yourdomain.com 2>&1 | grep -E "SSL certificate|subject|issuer|HTTP/"

# 3. Verify the app responds
curl -s -o /dev/null -w "%{http_code}" https://pm.yourdomain.com

# 4. Check database connectivity from the app container
docker compose exec leantime php -r "
  \$c = new mysqli(getenv('LEAN_DB_HOST'), getenv('LEAN_DB_USER'), getenv('LEAN_DB_PASSWORD'), getenv('LEAN_DB_DATABASE'));
  echo \$c->connect_error ? 'DB error: '.\$c->connect_error : 'DB OK';
"

# 5. Inspect logs for PHP fatal errors
docker compose logs leantime 2>&1 | grep -i "fatal\|error\|exception" | tail -20

Expected results: all containers show Up (healthy), TLS certificate is issued by Let's Encrypt, the HTTP status code is 200 or 302, and the database connectivity check prints DB OK.

Common issues and fixes

  • Container exits immediately with Access denied for user: MySQL credentials in .env do not match what MySQL was initialised with. Destroy the volume (docker compose down -v), correct the credentials, and restart. Note that MySQL only reads init variables on first volume creation.
  • Caddy returns 502 Bad Gateway: The leantime container is still starting or failed. Run docker compose logs leantime to diagnose. If the app is listening but Caddy still 502s, verify the port mapping — the container must be bound to 127.0.0.1:8080 and the Caddyfile must proxy to the same address.
  • HTTPS certificate not issued: Confirm that pm.yourdomain.com resolves to your server's public IP from an external DNS resolver (dig pm.yourdomain.com @1.1.1.1), and that port 80 is reachable from the internet. Caddy uses HTTP-01 ACME challenge on port 80 for initial certificate issuance.
  • File uploads fail or disappear after restart: Check that both leantime_uploads and leantime_public_uploads volumes exist (docker volume ls | grep leantime) and that the Compose file references them correctly. If you used docker compose down -v during troubleshooting, volumes were deleted along with containers.
  • Setup wizard loops or shows blank screen: Usually a PHP session issue. Confirm LEAN_SESSION_PASSWORD is set and not empty. Also check that LEAN_APP_URL matches the exact URL you are accessing — a mismatch between http and https or a missing trailing slash can cause redirect loops.
  • Email invitations not sent: Leantime will show a silent failure if SMTP variables are not configured. Add the SMTP block to .env and run docker compose restart leantime.

FAQ

What makes Leantime different from Plane or Vikunja for ADHD teams?

Leantime was designed from the ground up with neurodivergent workflows in mind. It includes built-in time-boxing (Pomodoro-style sprints), a distraction-reduced canvas view, and goal-linking that connects daily tasks directly to strategic objectives. Plane and Vikunja are excellent general-purpose tools but do not have these accessibility-focused features as first-class concepts.

Can I upgrade Leantime without losing data?

Yes. Since data lives in Docker named volumes, upgrading is a three-step process: pull the new image (docker compose pull), recreate the container (docker compose up -d --force-recreate leantime), and watch the logs for the automatic database migration. Always take a database dump before upgrading to a major version.

How do I back up and restore Leantime's data?

Take a MySQL dump and archive the two userfiles volumes:

# Back up database
docker compose exec db mysqldump -u root -p${MYSQL_ROOT_PASSWORD} leantime > /opt/leantime/backup_$(date +%F).sql

# Back up file uploads
docker run --rm -v leantime_uploads:/data -v /opt/leantime/backups:/backup \
  alpine tar czf /backup/uploads_$(date +%F).tar.gz -C /data .

# Restore database
docker compose exec -T db mysql -u root -p${MYSQL_ROOT_PASSWORD} leantime < /opt/leantime/backup_YYYY-MM-DD.sql

Can multiple teams share one Leantime instance?

Yes. Leantime supports multiple workspaces and role-based access control. You can create separate client spaces for different teams or customers, each with their own projects and timelines. Users can be assigned to multiple workspaces with different permission levels.

How do I enable LDAP or SSO authentication?

Leantime supports LDAP authentication out of the box through environment variables (LEAN_LDAP_USE_LDAP, LEAN_LDAP_HOST, etc.). For OIDC/SAML-based SSO, you can front Leantime with Authelia or Authentik as a forward-auth layer using Caddy's forward_auth directive. See the SysBrix guide for Authentik with Docker Compose and Caddy for the integration pattern.

How do I restrict access to internal networks only?

Modify the Caddyfile to add an IP range allow-list before the reverse_proxy directive:

pm.yourdomain.com {
    @blocked not remote_ip 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12
    respond @blocked 403
    reverse_proxy 127.0.0.1:8080
}

This allows access only from RFC 1918 private ranges, which is appropriate when Caddy is behind a VPN or internal load balancer.

How do I monitor container health in production?

The MySQL container already has a built-in health check in the Compose file. Add a Leantime health check by using Uptime Kuma to poll https://pm.yourdomain.com at a 60-second interval and alert via webhook, Telegram, or Slack on failure. For infrastructure-level visibility, deploy Prometheus and Grafana alongside the stack and scrape Docker container metrics with cAdvisor.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Uptime Kuma with Docker Compose + Caddy on Ubuntu
Self-hosted uptime monitoring with automatic HTTPS, multi-channel alerts, and a public status page — fully reproducible stack in under 30 minutes.