Skip to content

How to Deploy Vikunja 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 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.

Before you start
  • 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)
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 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
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 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.

Next steps

How to self-host Vikunja →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 Leantime 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 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.