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 caddyor 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/leantime2. 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_PASSWORDCreate /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 PMchmod 600 /opt/leantime/.env3. 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 caddy5. 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_PASSWORDperiodically. 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 theleantimedatabase. Do not userootfor 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
.envfrom 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.envand 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 -20Expected 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.envdo 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
leantimecontainer is still starting or failed. Rundocker compose logs leantimeto diagnose. If the app is listening but Caddy still 502s, verify the port mapping — the container must be bound to127.0.0.1:8080and the Caddyfile must proxy to the same address. - HTTPS certificate not issued: Confirm that
pm.yourdomain.comresolves 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_uploadsandleantime_public_uploadsvolumes exist (docker volume ls | grep leantime) and that the Compose file references them correctly. If you useddocker compose down -vduring troubleshooting, volumes were deleted along with containers. - Setup wizard loops or shows blank screen: Usually a PHP session issue. Confirm
LEAN_SESSION_PASSWORDis set and not empty. Also check thatLEAN_APP_URLmatches 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
.envand rundocker 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.sqlCan 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
- Production Guide: Deploy Vikunja with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Plane with Docker Compose + Caddy + PostgreSQL + Redis + MinIO on Ubuntu
- Production Guide: Deploy Authentik with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
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.