Skip to main content
Operations & Automation

Vikunja Self-Hosted Task Management: My Production Setup

How I deploy Vikunja as a self-hosted task manager for an agency: the Compose stack, the Nginx reverse proxy quirk, mail config, and when to skip Trello.

Published Updated 10 min read

I have been running Vikunja as the self-hosted task manager for my own agency work and a couple of small client teams since 2023, and the deployment has settled into a layout I would set up for a paying client tomorrow. This Vikunja self-hosted task management guide is the actual stack I ship: MariaDB, the Go API, the Vue frontend, and the Nginx sidecar that fuses the two into one origin, all behind Nginx Proxy Manager for TLS.

If you have bounced off Trello’s free-tier limits or you are tired of paying Asana 11€ per user per month for what is essentially a list of cards with due dates, Vikunja is worth a serious look. It is a small Go binary plus a Vue frontend, the database is MariaDB, and the entire production setup fits in roughly 1GB of RAM. The catch is the proxy topology. The official Compose file uses a sidecar Nginx to merge the API and frontend into one origin, and missing that detail is where most self-hosters get tripped up.

Roughly 20 to 30 minutes from a clean Docker host to a working Vikunja with TLS, an account, and your first kanban board.

Why self-host Vikunja instead of paying for Trello or Asana

The honest answer is that for a solo founder or a five-person team that just wants kanban, Trello’s free tier is fine. Asana’s free tier is fine. Most teams should pay for tools.

Vikunja earns its keep when one of these is true:

  1. Client confidentiality. Agency work under NDA on Trello means the client’s pre-launch roadmap sits on Atlassian’s servers. The GDPR-sensitive clients care.
  2. Per-seat economics break. Asana Business at 25€ per user per month for 15 people is 4,500€ per year. A Hetzner CX22 with daily snapshots runs around 70€ per year. The math starts working between 8 and 10 users.
  3. Integration with the rest of a self-hosted stack. If you already run Nextcloud for files, Authentik for SSO, and n8n for automation, Vikunja slots in as the task layer with shared identity and webhook triggers.
  4. You actually want CalDAV. Vikunja exposes every project as a CalDAV calendar. Subscribe from any standards-compliant calendar app and due dates appear next to real meetings without a Zapier in the middle.

If none of those apply, pay for Trello and move on. I am not going to pretend Vikunja is strictly better than a tool with a decade of product work behind it.

Prerequisites for the Vikunja self-hosted installation

A few non-negotiables before any of this lands on a server:

  • A hardened Linux host. SSH keys only, no root login, UFW with deny-by-default. My Linux server security fundamentals post is the baseline I run on every fresh box.
  • A reverse proxy stack. Vikunja sits behind a real reverse proxy with valid TLS. I use Nginx Proxy Manager from my Portainer + NPM + Vaultwarden stack. Caddy or Traefik work too; my notes assume NPM.
  • A real domain name with DNS access. Mostly so Let’s Encrypt can issue a cert via the proxy. CalDAV in particular hates self-signed certs across mobile clients.
  • An SMTP relay. Vikunja uses email for password resets, account confirmation, and reminders. I use the same SMTP relay I use for Mailcow notifications or a dedicated transactional account from any provider that gives you SMTP credentials. Without this, password resets silently fail.
  • Server sizing. 1GB RAM is enough for an agency-sized team, 2GB if you also run Portainer and NPM on the same box. CPU is rarely a bottleneck.

I run Vikunja for one team on a Hetzner CX22 (2 vCPU, 4GB RAM) shared with NPM, Portainer, and a few other small services. The Vikunja stack itself uses around 350MB of RAM in steady state.

Preparing the host directories

Vikunja’s official Compose file mounts three host paths: the MariaDB data directory, the Vikunja files directory, and an Nginx config file. Create them before bringing the stack up so the bind mounts do not silently create root-owned directories that the containers cannot write into.

mkdir -p /srv/docker/vikunja/db-data
mkdir -p /srv/docker/vikunja/files
cd /srv/docker/vikunja

Then write the Nginx sidecar config. This is the file that fuses the Vue frontend at / with the Go API at /api/, /dav/, and /.well-known/ so the browser sees one origin:

server {
    listen 80;

    location / {
        proxy_pass http://frontend:80;
    }

    location ~* ^/(api|dav|\.well-known)/ {
        proxy_pass http://api:3456;
        client_max_body_size 20M;
    }
}

Save this as nginx.conf in /srv/docker/vikunja/. The client_max_body_size 20M is the upload ceiling for task attachments. If your team uploads PDFs or screenshots regularly, bump it to 50M or 100M and bump the matching limit on your external NPM proxy host too.

The Vikunja Docker Compose file

Here is the actual Compose file I deploy. Every [YOUR VALUE] placeholder needs a real value before you bring it up; I keep them in a .env file in the same directory and reference them with ${VAR} in production, but the inline form below matches the upstream docs and is easier to read first time through.

version: "3"

services:
  db:
    image: mariadb:latest
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: [YOUR VALUE]
      MYSQL_USER: [YOUR VALUE]
      MYSQL_PASSWORD: [YOUR VALUE]
      MYSQL_DATABASE: [YOUR VALUE]
    volumes:
      - /srv/docker/vikunja/db-data:/var/lib/mysql
    restart: unless-stopped

  api:
    image: vikunja/api:latest
    environment:
      VIKUNJA_DATABASE_HOST: db
      VIKUNJA_DATABASE_PASSWORD: [YOUR VALUE]
      VIKUNJA_DATABASE_TYPE: mysql
      VIKUNJA_DATABASE_USER: [YOUR VALUE]
      VIKUNJA_DATABASE_DATABASE: [YOUR VALUE]
      VIKUNJA_SERVICE_ENABLETASKATTACHMENTS: 1
      VIKUNJA_SERVICE_ENABLEREGISTRATION: 1
      VIKUNJA_SERVICE_ENABLEEMAILREMINDERS: 1
      VIKUNJA_MAILER_ENABLED: 1
      VIKUNJA_MAILER_FORCESSL: 0
      VIKUNJA_MAILER_HOST: [YOUR VALUE]
      VIKUNJA_MAILER_PORT: 587
      VIKUNJA_MAILER_USERNAME: [YOUR VALUE]
      VIKUNJA_MAILER_PASSWORD: [YOUR VALUE]
      VIKUNJA_MAILER_FROMEMAIL: [YOUR VALUE]
      VIKUNJA_SERVICE_FRONTENDURL: [YOUR VALUE]
      VIKUNJA_SERVICE_TIMEZONE: [YOUR VALUE]
    volumes:
      - /srv/docker/vikunja/files:/app/vikunja/files
    depends_on:
      - db
    restart: unless-stopped

  frontend:
    image: vikunja/frontend:latest
    restart: unless-stopped

  proxy:
    image: nginx:latest
    ports:
      - 8022:80
    volumes:
      - /srv/docker/vikunja/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - api
      - frontend
    restart: unless-stopped

The values that matter most before you bring this up:

  • VIKUNJA_SERVICE_FRONTENDURL is the public URL with a trailing slash. Get this wrong and email links point at the wrong host. Example: https://tasks.youragency.com/.
  • VIKUNJA_SERVICE_TIMEZONE is the IANA name like Europe/Berlin. Reminders fire in this timezone, not the server’s.
  • The MariaDB MYSQL_* values must match the VIKUNJA_DATABASE_* values. I have lost an hour to a typo here.
  • The mailer block. If you do not have SMTP credentials yet, set VIKUNJA_MAILER_ENABLED: 0 for now. You can come back and edit this, but reminders and password resets will not work in the meantime.

Bring it up:

docker compose -f /srv/docker/vikunja/docker-compose.yml up -d

The proxy container exposes port 8022 on the host. That is the port your external NPM proxy host points at, not the API port and not the frontend port. The whole appeal of the sidecar topology is that you treat the stack as one HTTP service.

Warning: If you move Vikunja to a different domain later, you must update VIKUNJA_SERVICE_FRONTENDURL and restart the API container. Vikunja embeds this URL in email links, OAuth redirects, and CalDAV principal URLs. A stale value silently breaks half the integrations and produces no error in the logs.

Reverse proxy and TLS through Nginx Proxy Manager

In the NPM admin UI:

  1. Add a new Proxy Host. Domain: tasks.youragency.com. Forward hostname/IP: the Docker host running Vikunja (or the proxy container name if NPM is on the same Docker network). Forward port: 8022.
  2. Tick “Block Common Exploits” and “Websockets Support”. Vikunja’s frontend uses websockets for live task updates between collaborators.
  3. SSL tab: request a Let’s Encrypt cert, force SSL, enable HTTP/2, enable HSTS.
  4. Advanced tab: bump client_max_body_size 20M; (or whatever you set in the sidecar config) to match. The default NPM limit can throttle uploads before they reach Vikunja.

After saving the proxy host, browse to the domain. You should see the Vikunja login screen. Vikunja does not ship with a default account, so register the first user, then immediately disable open registration before anyone else finds the URL.

Locking down registration after the team is in

The Compose file ships with VIKUNJA_SERVICE_ENABLEREGISTRATION: 1 because the upstream tutorial assumes you need to register the first user. Once your team has accounts:

  1. Edit the Compose file and set VIKUNJA_SERVICE_ENABLEREGISTRATION: 0.
  2. docker compose up -d to recreate the API container with the new env value.
  3. Confirm the registration link disappears from the login page.

I once left this on for three weeks on a public Vikunja instance and ended up with 40 spam accounts to clean up. The bots find self-hosted task managers fast.

For ongoing user management, wire Vikunja’s OIDC support to Authentik and let your IdP handle account lifecycle. For an agency with more than five users, OIDC is the right call.

Backups, the part the install guide skips

Two things to back up: the MariaDB volume at /srv/docker/vikunja/db-data and the attachments at /srv/docker/vikunja/files. Attachments grow with team uploads, the database grows with tasks, comments, and revisions.

My production setup:

  • Hetzner volume snapshots nightly on the host volume.
  • A weekly mariadb-dump of the Vikunja database, gzipped and shipped to a separate Storage Box over restic.
  • A monthly tarball of the files directory to the same restic repository.

The non-negotiable: rehearse the restore on a fresh VPS. Restore the volumes, bring the Compose stack up, log in as a real user, verify attachments still load. Do this once before you have 14 months of agency tasks behind a backup you have never tested.

Vikunja in practice: what it is good at, what it is not

Some honest impressions from running it as my primary task manager since 2023.

What Vikunja does well:

  • Kanban boards are responsive, reorderable, and the keyboard shortcuts work. This is the daily-driver view for me.
  • Multiple project views (list, kanban, gantt, table) on the same tasks. Switching between them is one click.
  • CalDAV sync. Every project becomes a calendar feed. Subscribe from Apple Calendar and due dates sit next to your meetings.
  • The API is well-documented and stable. I have a handful of n8n workflows that create Vikunja tasks from inbound emails and Trustpilot reviews.

What Vikunja does not do well:

  • Document-heavy workflows. If you live in Notion and want a database with rich-text pages, Vikunja is not it.
  • Permissions complexity. Team and project sharing covers 80% of cases. Fine-grained per-field permissions or approval gates are not in the box.
  • Reporting. No native dashboards or burndown charts. Export to CSV and report from your BI tool if you need this.

Verifying the deployment before you trust it

A short checklist before you onboard real users:

  1. Register your admin account, log in, create a project, add three tasks.
  2. Trigger a password reset from the login page. The email should arrive within 30 seconds. If not, your SMTP block is wrong.
  3. Upload a 10MB attachment to a task. If it 413s, your client_max_body_size is wrong somewhere in the proxy chain.
  4. Subscribe to the project’s CalDAV URL from Apple Calendar or Thunderbird and verify due dates appear.
  5. Disable open registration and confirm the “Register” link is gone in an incognito window.
  6. Run a backup restore drill on a throwaway VPS. Confirm tasks and attachments come back.

Step 6 is the one most people skip. Do it once before your real backlog is on the line.

Closing the loop

For an agency that needs a task manager that respects client confidentiality, scales past Trello’s free tier without paying per seat, and exposes a real API plus CalDAV, Vikunja is the boring, working choice. The stack above runs on a 4€-per-month VPS and has been stable for me across 18 months of upgrades.

Pair it with Authentik for SSO, Nextcloud for files, and Uptime Kuma watching the proxy and you have a self-hosted productivity spine that holds up under day-to-day agency work.

Recurring cost on my setup is roughly 6€ per month for the VPS share plus the SMTP relay. The monthly burden is reading the backup log on the first Monday and pulling new image tags when Vikunja ships a release. That is the deal: a few minutes a month, in exchange for owning your task layer end to end.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access