How to Deploy Leantime on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-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.
- 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)
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
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'ssample.envcover it. - Single sign-on: LDAP and OIDC are both in the free edition, configured
with
LEAN_LDAP_*andLEAN_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.