What you end up with
PLANKA is a kanban board in the style of Trello: projects hold boards, boards hold lists, lists hold cards, and changes show up for everyone without a refresh. Its docs list markdown descriptions, attachments, comments, custom fields, due dates, a stopwatch, webhooks, a REST API, two-factor sign-in and notifications through more than a hundred services in the free Community edition.
- The
plankacontainer runs the app, listening on 1337 inside its container; the official file maps that to 3000 on the host. - The
postgrescontainer keeps the boards, cards and users in Postgres 16. - A volume named
datakeeps uploaded files: attachments, avatars and background pictures. - A small proxy on the host (Caddy here) answers on 443 with a real certificate.
The PLANKA documentation does not list hardware requirements, so we will not invent any. What will grow over time is disk, from the files your team attaches, so leave room for those and for a couple of weeks of local backups.
Does a kanban board need a whole server?
It needs one you control, and a small whole machine is often the simplest way to get that. You get root, nobody else's work on the hardware, and an unmetered port, so a busy week of uploads never shows up on the invoice. You manage the server; we keep the hardware and the network running.
Pick a compact model from our dedicated server range. If you want a console you can open yourself after a firewall change shuts you out, choose a rack server, a blade, or the m510 or m710x cartridge; on other cartridges you get the power control and nothing more. Pass on the X-Gene ARM model; Ubuntu 16.04 and Ubuntu 18.04 are all it offers. A ready server is yours within 30 minutes of approval, and a custom build takes hours, not minutes. One IPv4 address is included and IPv6 costs nothing, which you will want for the AAAA record.
Already running other apps in containers? Containers on bare metal explains how we approach that, and the Vikunja page covers a list-style task app if cards are not your team's habit.
The three lines you must change
The official file is set up for a quick test (BASE_URL is localhost and the key is a published example), which is why it is unsafe on a public server as shipped. Open it and change these before the first start:
- BASE_URL: from
http://localhost:3000to your real address, for examplehttps://board.example.com. A comment in PLANKA'sserver/config/env/production.jsexplains that when this differs from the address in the browser, the page appears, but each request after it gets turned away. - SECRET_KEY: replace
notsecretkeywith a fresh value fromopenssl rand -hex 64, the command the install guide gives. The file's own comment warns that while the example value stays, anyone could forge a sign-in token for any account. - ports: change
3000:1337to127.0.0.1:3000:1337(our recommendation, not the shipped default) so only Caddy, running beside it on that machine, can connect. - While you are there, uncomment
TRUST_PROXY=true. The fileserver/config/env/production.jsmaps it totrustProxy, so PLANKA reads the forwarded headers Caddy sends, which carry the visitor's address and the fact the visit was HTTPS.
Why the 127.0.0.1 matters: Docker publishes a bare mapping such as 3000:1337 on every address of the host, both 0.0.0.0 and [::]. Over IPv4 that traffic never meets ufw's rules. Over IPv6 the same holds when Docker runs its own IPv6 networking. Without that, a docker-proxy process listens there instead, and ufw can filter that. Prefixing 127.0.0.1 keeps the port private from Docker Engine 28 onward (moby#45610). Caddy is a normal host program, so ufw must admit ports 80 and 443 for it, over IPv4 and IPv6.
Up and running in an evening
- Point your name at the server first: an A record, plus AAAA if you took IPv6. Caddy can only get a certificate once that resolves.
- Get Docker Engine (28 is the minimum we suggest) plus its Compose plugin, set up the way Docker describes for your system.
- Run the commands below as root, top to bottom. The firewall is up before anything listens, and Caddy fetches its certificate before PLANKA is configured. The
printfline writes a Caddyfile modelled on the one in the PLANKA docs, without its optional email block and pointed at 127.0.0.1:3000. - When
ls -lshows-rw-------, paste the key and make the changes above in nano. - The account script has four prompts:
Email:first, thenPassword:,Name:, and lastUsername (optional):. Its code,server/db/create-admin-user.js, runs each password through the zxcvbn strength check and asks again when it is weak. If that email belongs to someone already, it lists the account and promptsUpdate this account? [y/N]; answering y overwrites that account's password, name and username, grants it admin rights and, if it had been deactivated, switches it back on. - After
docker compose up -d, open your address and sign in with that email and password.
# firewall first: IPv6 on, your SSH login kept, then web traffic
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
ufw allow 80,443/tcp
# Caddy fetches the HTTPS certificate once DNS points here
apt update && apt install -y caddy
printf '%s\n' 'board.example.com {' ' reverse_proxy 127.0.0.1:3000 {' ' header_up X-Forwarded-Proto {scheme}' ' }' '}' > /etc/caddy/Caddyfile
systemctl restart caddy
# PLANKA's file, locked down before the secret key is added
mkdir -m 700 /opt/planka
cd /opt/planka
install -m 600 /dev/null docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml -o docker-compose.yml
ls -l docker-compose.yml
openssl rand -hex 64
nano docker-compose.yml
# your admin account, made before the board answers anyone
docker compose run --rm planka npm run db:create-admin-user
docker compose up -d
The PLANKA Caddy page says you cannot drop the header_up X-Forwarded-Proto {scheme} line if your sign-ins depend on secure cookies and sessions. Its example has no websocket settings; Caddy's reverse_proxy passes the upgrade that PLANKA's live updates use without extra lines (our note, not a claim from the PLANKA docs). Nginx needs explicit Upgrade and Connection headers, shown on PLANKA's nginx page.
Your first admin, made in private
PLANKA has not created an administrator automatically since version 1.13, so someone has to make one. Doing it with the script, before docker compose up -d, means the account exists before the board answers anybody, and the password never sits in the Compose file.
The other documented route is the four DEFAULT_ADMIN_ variables (EMAIL, PASSWORD, NAME, USERNAME) in the Compose file. The docs say they can come out after the first successful start, and that leaving DEFAULT_ADMIN_EMAIL in place stops other people editing or deleting that admin. If you go that way, the file is already private, but the password stays in it until you remove it.
There is no open sign-up page described in the docs. According to server/api/controllers/users/create.js, creating a user through the API needs admin rights, so you add your colleagues yourself. Signing in through your company's identity provider (SSO with OIDC) and guest roles are listed as Pro features, not part of Community.
Nightly backups that include your attachments
A database dump alone is only half your board: the pictures and files on cards live in the data volume. PLANKA's own backup script copies both, and its comments warn that the archive holds password hashes, sessions, two-factor secrets and mail credentials, so treat it like the database itself. Ours does the same job in a cron-friendly way (no terminal flag, failures stop the run, each part is checked) and also keeps the Compose file, which holds your SECRET_KEY. With the folder at /opt/planka, Docker names the volume planka_data; docker volume ls shows it.
# save as /root/backup-planka.sh
set -euo pipefail
umask 077
OUT=/var/backups/planka/$(date +%F)
DC="docker compose -f /opt/planka/docker-compose.yml"
mkdir -p "$OUT"
$DC exec -T postgres pg_dump -U postgres -Fc planka > "$OUT/planka.dump"
$DC exec -T postgres pg_restore --list < "$OUT/planka.dump" > /dev/null
docker run --rm -v planka_data:/src:ro -v "$OUT":/dst alpine tar -czf /dst/files.tgz -C /src .
tar -tzf "$OUT/files.tgz" > /dev/null
cp /opt/planka/docker-compose.yml "$OUT/compose.yml"
find /var/backups/planka -mindepth 1 -maxdepth 1 -mtime +14 -exec rm -rf {} +
rsync -a /var/backups/planka/ copies@second-box.example.org:planka-backups/
- The last line pushes copies to another machine, so give root a key first, or the job will sit waiting for a password. As root, make the key with
ssh-keygen -t ed25519and install it on the other machine withssh-copy-id copies@second-box.example.org. Both machines need rsync. - Make the log folder once:
mkdir -m 700 /var/log/backup-planka. - Create
/etc/cron.d/backup-plankaholding this single line:15 3 * * * root umask 077; bash /root/backup-planka.sh > /var/log/backup-planka/$(date +\%F).log 2>&1.
Getting a backup back
Restoring replaces whatever is running now, as PLANKA's docs warn, so this keeps today's data first. Set OUT to the backup day you want, run the lines down to the comment, and only continue if you saw backup-ok, db-copy-ok and files-copy-ok. The files go back to uid 1000, since the official image is made to run as that user.
set -o pipefail
OUT=/var/backups/planka/2026-10-20
DC="docker compose -f /opt/planka/docker-compose.yml"
SAFE=/var/backups/planka/before-restore
mkdir -m 700 "$SAFE"
tar -tzf "$OUT/files.tgz" > /dev/null && $DC exec -T postgres pg_restore --list < "$OUT/planka.dump" > /dev/null && echo backup-ok
$DC stop planka
$DC exec -T postgres pg_dump -U postgres -Fc planka > "$SAFE/planka.dump"
$DC exec -T postgres pg_restore --list < "$SAFE/planka.dump" > /dev/null && echo db-copy-ok
docker run --rm -v planka_data:/src:ro alpine tar -czf - -C /src . > "$SAFE/files.tgz"
tar -tzf "$SAFE/files.tgz" > /dev/null && echo files-copy-ok
# carry on only if all three lines above said ok
$DC exec -T postgres pg_restore -U postgres -d planka --clean --if-exists < "$OUT/planka.dump"
docker run --rm -v planka_data:/dst -v "$OUT":/in:ro alpine sh -c 'find /dst -mindepth 1 -delete && tar -xzf /in/files.tgz -C /dst && chown -R 1000:1000 /dst'
$DC start planka
Sign in and check a card that has a file attached. When everything looks right, delete the before-restore folder. If it does not, stop PLANKA again with $DC stop planka and rerun the last three commands with OUT=$SAFE. Moving to a new server? Put the saved compose.yml back as /opt/planka/docker-compose.yml with mode 600, run docker compose create and docker compose start postgres from that folder, wait until docker compose ps calls postgres healthy, set OUT and DC as in the block, and run it from the pg_restore line on.
Keeping PLANKA up to date
Take a backup first. Then, inside /opt/planka: docker compose down stops everything, docker compose pull fetches whatever release is newest, and docker compose up -d brings it back. That is the entire update procedure in the docs, and because the file uses the latest tag, there is no version number to edit.
Coming from version 1 is a bigger job. You need 1.26.2 or later first, and the docs are clear that without a proper backup there is no way back. Version 2 moves everything from the three old upload volumes (avatars, project backgrounds, attachments) into the one data volume, needs npm run db:upgrade before its first start, and only accepts true or false for TRUST_PROXY. Follow the Docker upgrade guide in the sources step by step, including the optional step that brings back which due dates were marked complete.
What the licence lets you do
PLANKA's licence guide describes its licences as fair-code. What follows is how we read it, and no substitute for a lawyer: LICENSE.md and the licence guide are what count, and PLANKA itself answers the edge cases. LICENSE.md in the repository is the PLANKA Community License 1.1 (updated May 20, 2025), which puts most of the code under a Fair Use License; files marked Pro/Enterprise, or with .pe. in the name, need a commercial licence. At the v1.26.2 tag, the file named LICENSE holds the AGPL-3.0 text.
- Fine for free: your own projects, your hobby, teaching, your company's internal work, a typical number of friends and family, free access for non-profits, and public educational institutions for academic or research purposes.
- Not fine without a paid licence: running it as a hosted service for others for commercial gain, or sharing accounts with outsiders for business. The licence guide even rules out giving logins to a client you work for.
- Always: keep PLANKA's licence and copyright notices, include the licence terms whenever you give someone a copy, and say clearly when you have modified it.
Questions people ask
- Is PLANKA really free if I host it myself?
- Yes, for the uses its Fair Use License allows, such as your own team's internal work or a personal project. The server is the only bill. Hosting it for paying customers needs PLANKA's commercial licence.
- Can I invite a client to a board?
- Not under the free licence when you have a business relationship with that client. PLANKA's licence guide uses that exact situation as an example that needs a Pro/Enterprise licence.
- Why do cards stop updating live behind my proxy?
- Live updates travel over socket.io websockets, which means your proxy must let the upgrade through, and BASE_URL has to be identical to what visitors type into the address bar. Caddy passes upgrades on its own (our note; PLANKA's Caddy example simply has no websocket lines), while nginx needs the Upgrade and Connection headers from PLANKA's nginx example.
- How do I update a Docker install of PLANKA?
- Make a backup, then in /opt/planka stop it (
docker compose down), pull the latest release (docker compose pull), then start it (docker compose up -d). Version 1 installs follow the separate v2 upgrade guide instead. - Where are attachments stored?
- In version 2 they sit in the
datavolume with avatars and background images, not in the database. That is why a backup needs the volume as well as a database dump.