How to Deploy Vikunja on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Vikunja on a small VPS — the task manager and its Postgres database in one compose file, behind HTTPS, with registration closed once your team is in.
- A VPS with at least 1 GB RAM
- 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 Vikunja is
Vikunja is an open-source to-do and task manager. Tasks live in projects, and every project can be viewed as a list, table, kanban board or Gantt-style timeline, with labels, filters, reminders and recurring tasks on top. It is written in Go with a Vue.js frontend, licensed AGPL-3.0, and rated 2 / 5 to deploy.
Since the 2.x releases the API and the web frontend ship as one image,
vikunja/vikunja. That makes the deployment short: one application container,
one database container, one directory for attachments. The free self-hosted
build includes the full task-management feature set, including SSO/LDAP login;
a separate paid Pro add-on (admin panel, audit logs, time tracking) is not part
of it and is not needed for anything in this guide.
If you are choosing between task tools, Vikunja vs Leantime compares it with a heavier, project-manager-oriented alternative, and Plane is the option for teams that want sprint cycles and roadmaps.
Server sizing
Vikunja is a single Go binary, and it is one of the lightest apps in the catalog:
- Minimum: 1 GB RAM — the catalog's floor for Vikunja plus Postgres, and enough for a small team.
- Measured: on a GCP e2-standard-2 running Ubuntu 26.04 with Docker 29.8.1, the idle stack (Vikunja + Postgres) used ~45 MB of RAM and ~568 MB of disk for images and data (September 2026).
Most of a 1 GB box is headroom for Postgres's cache, the reverse proxy and the operating system. Disk grows with attachments, not tasks — budget for whatever files your team uploads.
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. Vikunja's own port, 3456, 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 Vikunja (Docker Compose)
Create the project directory and the two data directories. Vikunja runs as
user 1000 and will not take ownership of its files directory itself, so
upstream's instructions are to create it and chown it before the first start:
mkdir -p ~/vikunja/files ~/vikunja/db && cd ~/vikunja
sudo chown 1000 files
Generate the secrets once, into a .env file that Compose reads
automatically. VIKUNJA_SERVICE_SECRET signs login tokens; the database
password is only ever used between the two containers:
cd ~/vikunja
if [ ! -f .env ]; then
printf 'VIKUNJA_SECRET=%s\nDB_PASSWORD=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 16)" > .env
chmod 600 .env
fi
Now the compose file. The image tag is pinned to the release the catalog was
verified with; bump it deliberately rather than riding latest:
cd ~/vikunja
cat > docker-compose.yml <<'YAML'
services:
vikunja:
image: vikunja/vikunja:2.6.0
restart: unless-stopped
environment:
# The public URL, with a trailing slash. Change it to your domain.
VIKUNJA_SERVICE_PUBLICURL: https://tasks.example.com/
VIKUNJA_SERVICE_SECRET: ${VIKUNJA_SECRET}
VIKUNJA_DATABASE_TYPE: postgres
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_PASSWORD: ${DB_PASSWORD}
VIKUNJA_DATABASE_DATABASE: vikunja
ports:
# Loopback only — Caddy is the sole route in from outside.
- "127.0.0.1:3456:3456"
volumes:
- ./files:/app/vikunja/files
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: vikunja
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: vikunja
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vikunja"]
interval: 5s
retries: 10
YAML
Start it and wait for the API to answer:
cd ~/vikunja
docker compose up -d
timeout 180 bash -c 'until curl -fs http://127.0.0.1:3456/api/v1/info >/dev/null; do sleep 3; done'
curl -s http://127.0.0.1:3456/api/v1/info | head -c 300; echo
docker compose ps
/api/v1/info returns the version and the enabled features as JSON. If it
answers, the database migrations ran and Vikunja is up. Postgres waits on its
health check before Vikunja starts, so the first boot is not a race.
HTTPS + domain
Point an A record for tasks.example.com at the server's public IP, wait
for it to resolve, then terminate TLS in front of 127.0.0.1:3456. The
straightforward path is Automatic HTTPS with
Caddy:
tasks.example.com {
reverse_proxy 127.0.0.1:3456
}
Make sure VIKUNJA_SERVICE_PUBLICURL in the compose file matches this URL
exactly, https:// and trailing slash included, then run docker compose up -d
again to apply it. Vikunja uses that value to build links in emails and to
serve the frontend its API address, so a mismatch shows up as a login page that
cannot reach the API.
If you use nginx instead, upstream's example sets client_max_body_size 20M
in the proxy block; if you raise Vikunja's upload limit, raise that too, or
large attachments fail at the proxy. Caddy has no default body limit.
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 vikunja:3456, and drop the
host port publish.
First login and closing registration
Open https://tasks.example.com and register. Registration is open by
default so the first accounts can be created, which also means anyone who
finds the URL can sign up. Once everyone who needs an account has one, turn it
off with the documented service.enableregistration option — as an
environment variable, add this line to the vikunja service's environment
block:
VIKUNJA_SERVICE_ENABLEREGISTRATION: "false"
then apply it with docker compose up -d. New people can still be added later
by temporarily re-enabling registration, or by connecting Vikunja to an
OpenID Connect provider (auth.openid.*) so accounts come from your identity
system instead — Authentik and
Keycloak both work.
Two more settings worth knowing about:
- Two-factor authentication (TOTP) is enabled by default
(
service.enabletotp). Ask every user to turn it on under their settings. - Link sharing lets anyone with a link see a project. It is on by default;
set
VIKUNJA_SERVICE_ENABLELINKSHARING: "false"if you never want projects readable without an account.
Email (reminders, password resets) needs SMTP settings under mailer.*;
without them, reminders stay in the web UI only.
Backups
Upstream's backup guidance is short: back up the database and the attachment files, and nothing else. It also warns that project deletion is permanent — there is no trash or undo — which makes backups the only recovery path for a deleted project.
cd ~/vikunja
mkdir -p backups
docker compose exec -T db pg_dump -U vikunja vikunja | gzip > backups/vikunja-db-$(date +%F).sql.gz
sudo tar czf backups/vikunja-files-$(date +%F).tar.gz files
ls -lh backups
pg_dump runs inside the database container, so the backup is consistent
without stopping anything. Copy the backups directory off the box —
object storage, another server, anywhere that doesn't share this VPS's fate —
and keep .env alongside it; without VIKUNJA_SECRET every session is
invalidated on restore.
To restore the database into a fresh stack, pipe the dump back into psql:
gunzip -c backups/vikunja-db-2026-09-29.sql.gz | docker compose exec -T db psql -U vikunja vikunja
Upgrades
Read the release notes, back up, then change the image tag in
docker-compose.yml and recreate:
cd ~/vikunja
docker compose pull
docker compose up -d
docker compose ps
Vikunja runs its database migrations on start, so there is no separate migrate step. Because they run automatically, take the backup first — a migration cannot be rolled back by switching the tag back.
Troubleshooting
The container exits with a permission error on /app/vikunja/files. The
files directory is not owned by UID 1000. Run sudo chown 1000 files in
~/vikunja and start it again. Rootless Docker maps UIDs differently; upstream's
workaround there is to run the container as 0:0.
The page loads but login fails or says it can't reach the API. Check that
VIKUNJA_SERVICE_PUBLICURL is the exact URL in your browser, with https://
and the trailing slash, and that you recreated the container after editing it.
docker compose up hangs on the database. Read docker compose logs db.
The usual cause is a db directory left over from an earlier attempt with a
different password — Postgres only reads POSTGRES_PASSWORD on first
initialisation.
Uploads fail for large files. Raise Vikunja's files.maxsize and, if
nginx is in front, its client_max_body_size to match.
Verification + next steps
You're done when you can: load https://tasks.example.com over a valid
certificate, log in, create a project and see it in list, kanban and Gantt
views, confirm that the registration link is gone after you disabled it, and
restore last night's pg_dump into a scratch stack.
From there: wire up SMTP so reminders arrive by email, connect an OIDC provider if you already run one, and point a CalDAV client at your domain. For hosting options, see Best VPS for Self-Hosting.