Skip to content

How to Deploy Leantime on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Leantime on a VPS — the official image with MySQL, generated secrets, HTTPS through Caddy, and backups of both the database and the uploaded files.

Before you start
  • A VPS with at least 1 GB RAM (2 GB is more comfortable)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain or subdomain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Leantime is

Leantime is an open-source project management system aimed at people who manage projects without being full-time project managers, with a stated focus on neurodivergent teams. It combines task and kanban boards, Gantt-style milestones, sprints, time tracking and a project wiki. It is written in PHP with a MySQL database, licensed AGPL-3.0, and rated 2 / 5 to deploy.

The free self-hosted edition includes the core project-management features plus white-labeling, client management and LDAP/OIDC login. Recurring tasks, custom fields, whiteboards, AI-assisted status updates and portfolio-level strategy tools are paid plugins or Pro-tier add-ons, so check that list before you commit a team to it.

For a lighter, task-first tool, compare it with Vikunja in Vikunja vs Leantime — or go straight to Deploy Vikunja on a VPS.

Server sizing

  • Minimum: 1 GB RAM — the catalog's floor for Leantime plus MySQL.
  • Measured: on a GCP e2-standard-2 running Ubuntu 26.04 with Docker 29.8.1, the idle stack (Leantime + MySQL 8.4) used ~529 MB of RAM and ~2.1 GB of disk for images and data (September 2026).

Most of that idle memory is MySQL, which reserves its buffer pool up front. On a 1 GB box that leaves little room for anything else; 2 GB is the comfortable size if the same server also runs the reverse proxy and a couple of other services. Budget disk for uploaded files on top of the ~2 GB baseline.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root deploy user and a ufw firewall. If not, work through Docker & Compose on Ubuntu first.

Open SSH and the reverse proxy ports only. Leantime listens on 8080 inside the container, and that stays on the loopback interface:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Leantime (Docker Compose)

Create the project directory and generate the three secrets once, into a .env file that Compose reads automatically. LEAN_SESSION_PASSWORD salts sessions — upstream's sample file ships a fixed example value and tells you to replace it:

mkdir -p ~/leantime && cd ~/leantime
if [ ! -f .env ]; then
  printf 'MYSQL_ROOT_PASSWORD=%s\nDB_PASSWORD=%s\nSESSION_PASSWORD=%s\n' \
    "$(openssl rand -hex 16)" "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" > .env
  chmod 600 .env
fi

Now the compose file. It follows upstream's docker-leantime setup: the official leantime/leantime image pinned to a release, MySQL 8.4 with the UTF8MB4 character set, and named volumes for the four directories upstream says to persist. Named volumes matter here — Docker seeds an empty named volume with the image's own file ownership, which avoids the "Operation not permitted" errors that bind-mounted directories are prone to.

cd ~/leantime
cat > docker-compose.yml <<'YAML'
services:
  db:
    image: mysql:8.4
    restart: unless-stopped
    command: --character-set-server=UTF8MB4 --collation-server=UTF8MB4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: leantime
      MYSQL_USER: leantime
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5

  leantime:
    image: leantime/leantime:3.9.8
    restart: unless-stopped
    environment:
      # Your public URL, including https://. Change it to your domain.
      LEAN_APP_URL: https://pm.example.com
      LEAN_DB_HOST: db
      LEAN_DB_USER: leantime
      LEAN_DB_PASSWORD: ${DB_PASSWORD}
      LEAN_DB_DATABASE: leantime
      LEAN_DB_PORT: "3306"
      LEAN_SESSION_PASSWORD: ${SESSION_PASSWORD}
      # Cookies over HTTPS only — correct once Caddy is in front.
      LEAN_SESSION_SECURE: "true"
    ports:
      # Loopback only — Caddy is the sole route in from outside.
      - "127.0.0.1:8080:8080"
    volumes:
      - public_userfiles:/var/www/html/public/userfiles
      - userfiles:/var/www/html/userfiles
      - plugins:/var/www/html/app/Plugins
      - logs:/var/www/html/storage/logs
    depends_on:
      db:
        condition: service_healthy

volumes:
  db_data:
  public_userfiles:
  userfiles:
  plugins:
  logs:
YAML

Start it and wait for the web server to answer:

cd ~/leantime
docker compose up -d
timeout 300 bash -c 'until curl -so /dev/null http://127.0.0.1:8080/; do sleep 5; done'
curl -so /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8080/install
docker compose ps

MySQL initialises its data directory on the first start, which takes longer than later boots; the health check holds Leantime back until the database accepts connections.

HTTPS + domain

Point an A record for pm.example.com at the server's public IP, wait for it to resolve, then terminate TLS in front of 127.0.0.1:8080. The straightforward path is Automatic HTTPS with Caddy:

pm.example.com {
    reverse_proxy 127.0.0.1:8080
}

Two settings in the compose file depend on this step. LEAN_APP_URL must be the exact public URL, because upstream documents it as required for proxy installs. LEAN_SESSION_SECURE: "true" makes the session cookie HTTPS-only — right behind Caddy, but it means login will not stick if you browse to the plain http:// port. If you want to test over plain HTTP before the domain is ready, set it to "false" temporarily and recreate the container.

If you run Caddy as a container, 127.0.0.1 is the proxy's own loopback. Put both in one compose network, use reverse_proxy leantime:8080, and drop the host port publish.

First-run setup

Open https://pm.example.com. On a fresh database Leantime sends you to its installer, where you enter the company name and create the first admin account. The installer creates the database tables, so nothing works until it has run — do it immediately after the first start, before someone else finds the page.

After that:

  • Invite users from the admin area rather than sharing the admin account.
  • Configure email. Invitations and password resets need SMTP; the LEAN_EMAIL_* variables in upstream's sample.env cover it.
  • Single sign-on: LDAP and OIDC are both in the free edition, configured with LEAN_LDAP_* and LEAN_OIDC_* variables. If you run an identity provider such as Authentik, use it.

Backups

There are two things to keep: the MySQL database and the uploaded files in the userfiles and public_userfiles volumes (logos and attachments).

cd ~/leantime
mkdir -p backups
docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction leantime' | gzip > backups/leantime-db-$(date +%F).sql.gz
for v in userfiles public_userfiles; do
  docker run --rm -v leantime_$v:/data -v "$PWD/backups":/backup alpine \
    tar czf /backup/leantime-$v-$(date +%F).tar.gz -C /data .
done
ls -lh backups

--single-transaction takes a consistent InnoDB snapshot without stopping anything. The volume names are prefixed with the project directory name (leantime_); confirm with docker volume ls if yours differ. Copy the archives and .env off the box.

To restore a dump into a running stack:

gunzip -c backups/leantime-db-2026-09-29.sql.gz | docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" leantime'

Upgrades

Back up first, read the release notes, then change the leantime/leantime tag in docker-compose.yml and recreate:

cd ~/leantime
docker compose pull
docker compose up -d
docker compose ps

Pin tags and move them on purpose; latest upgrades you whenever the container is recreated, which is not when you want a schema change to happen.

Troubleshooting

"Operation not permitted" or upload errors. Upstream's first answer is volume ownership. The published image runs as UID 1000; with named volumes that is handled for you. If you switched to bind mounts, chown -R 1000 the host directories, or fix ownership inside the container as upstream's README shows.

Login succeeds, then you are logged straight out. LEAN_SESSION_SECURE is true and you are on plain HTTP. Use the HTTPS URL, or set it to false while testing.

Links, redirects or assets point at the wrong host. LEAN_APP_URL does not match the URL in your browser. Fix it and run docker compose up -d.

Leantime cannot reach the database. Read docker compose logs db. A db_data volume left over from an earlier attempt keeps its original passwords, because MySQL only reads them on first initialisation — remove the volume (if it holds nothing you need) and start again.

Verification + next steps

You're done when you can: load https://pm.example.com over a valid certificate, finish the installer, log in as the admin, create a project with a few tasks on the kanban board, upload an attachment, and restore last night's dump into a scratch stack.

From there: connect SMTP, wire LDAP or OIDC, and add team members. If you need heavier agile tooling, Plane, OpenProject and Taiga are the next steps up. For hosting options, see Best VPS for Self-Hosting.

Next steps

How to self-host Leantime →More self-hosted project management tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.