$linuxjunkies
>

Self-Host Vikunja for To-Do and Projects

Deploy Vikunja, a self-hosted open-source task manager, using Docker Compose with PostgreSQL, set up sharing, connect the mobile app, and compare it to Todoist.

BeginnerUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Docker Engine ≥ 24 and Docker Compose v2 installed
  • A domain or subdomain with DNS pointing to your server's IP
  • Ports 80 and 443 open in your firewall or security group
  • Sudo or root access on the server

Vikunja is a self-hosted, open-source task manager that handles personal to-do lists and multi-user project boards without sending your data to a third-party SaaS. It covers the same ground as Todoist or Tick Tick — lists, due dates, priorities, labels, Kanban boards, Gantt charts, reminders — but runs entirely on your own server. This guide walks through a production-ready Docker Compose deployment, configuring file sharing and user management, pairing the Android/iOS app, and an honest comparison with Todoist so you can decide whether self-hosting is worth it for your situation.

Prerequisites

  • A Linux server (VPS or home lab) with Docker Engine ≥ 24 and Docker Compose v2 installed
  • A domain name or subdomain pointed at the server's public IP (e.g., tasks.example.com)
  • Ports 80 and 443 reachable from the internet, or a reverse proxy already running (Caddy, Nginx Proxy Manager, Traefik)
  • Root or sudo access

Understanding the Vikunja Stack

Vikunja ships as a single Go binary that serves both the REST API and the optional built-in frontend. Since version 0.22 the frontend is bundled into the same container image (vikunja/vikunja), so you no longer need separate api and frontend containers. You still need a database; PostgreSQL is recommended for anything beyond personal use. Optionally add a Redis container for session caching under load.

Step 1 — Create the Project Directory

Pick a persistent location. /opt/vikunja is conventional for server-side apps.

sudo mkdir -p /opt/vikunja/{files,db}
sudo chown -R $USER:$USER /opt/vikunja
cd /opt/vikunja

Step 2 — Write docker-compose.yml

Create the Compose file. Replace CHANGE_ME_DB_PASS and CHANGE_ME_JWT with strong random strings before deploying. Generate them with openssl rand -hex 32.

cat > docker-compose.yml <<'EOF'
services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: vikunja
      POSTGRES_PASSWORD: CHANGE_ME_DB_PASS
      POSTGRES_DB: vikunja
    volumes:
      - ./db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vikunja"]
      interval: 10s
      timeout: 5s
      retries: 5

  vikunja:
    image: vikunja/vikunja:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "3456:3456"
    environment:
      VIKUNJA_DATABASE_TYPE: postgres
      VIKUNJA_DATABASE_HOST: db
      VIKUNJA_DATABASE_DATABASE: vikunja
      VIKUNJA_DATABASE_USER: vikunja
      VIKUNJA_DATABASE_PASSWORD: CHANGE_ME_DB_PASS
      VIKUNJA_SERVICE_JWTSECRET: CHANGE_ME_JWT
      VIKUNJA_SERVICE_FRONTENDURL: https://tasks.example.com/
      VIKUNJA_MAILER_ENABLED: "false"
    volumes:
      - ./files:/app/vikunja/files
EOF

A few notes on key environment variables:

  • VIKUNJA_SERVICE_FRONTENDURL — must match your public URL exactly; Vikunja embeds it in invitation emails and magic links.
  • VIKUNJA_MAILER_ENABLED — set to true and fill in SMTP credentials if you want email notifications and password resets. See the official docs for the full mailer variable list.
  • files volume — attachments and background images are stored here; back this up alongside the database.

Caddy handles TLS automatically via Let's Encrypt. If you already run Nginx or Traefik, adapt accordingly — the upstream is simply http://localhost:3456.

sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
tasks.example.com {
    reverse_proxy localhost:3456
}
EOF
sudo systemctl reload caddy

Step 4 — Start Vikunja

docker compose up -d

Watch the logs until the API reports it is listening:

docker compose logs -f vikunja

You should see a line similar to ⇨ http server started on [::]:3456 within 15–20 seconds. Open https://tasks.example.com in a browser to confirm the login screen loads.

Step 5 — First Login and Admin Setup

Register the first account through the web UI at /register. The first registered user automatically becomes the instance admin. Subsequent registrations are open by default; to restrict them, add this variable to the vikunja service environment block and redeploy:

VIKUNJA_SERVICE_ENABLEREGISTRATION: "false"
docker compose up -d vikunja

After that, invite users manually from the admin panel at /admin.

Step 6 — Sharing Projects and Tasks

Vikunja has three sharing mechanisms worth knowing:

  • Team sharing — create a team under your account settings, add members by username, then share any project with that team at a chosen permission level (read, write, or admin). Best for ongoing collaboration.
  • Per-user sharing — share a single project directly with another registered user. Go to the project's settings, choose Share, search by username, and set the permission level.
  • Public share links — generate a read-only or read-write link for a project that anyone with the URL can access without an account. Find this under project settings → Share links. Useful for sharing a task board with someone who will not create an account.

File attachments (PDFs, images, etc.) can be uploaded directly to individual tasks. They land in the ./files directory on the host, which is why backing up that volume matters as much as the database.

Step 7 — Connect the Mobile App

Vikunja publishes official apps for Android (Play Store and F-Droid) and iOS (App Store). The setup is identical on both platforms:

  1. Open the app and tap Use a custom server.
  2. Enter your full URL: https://tasks.example.com. Include the trailing path if you deployed under a subpath.
  3. Log in with your username and password. The app uses the same JWT-based API as the web UI.

The mobile apps support offline task creation; changes sync when connectivity returns. Push notifications require a third-party relay service (Gotify or ntfy) configured via the Vikunja webhook system — not covered here, but the official docs have a dedicated page for it.

Vikunja vs. Todoist — Honest Comparison

FeatureVikunja (self-hosted)Todoist (SaaS)
CostFree (hosting costs only)Free tier; Pro ~$4/mo
Data ownershipComplete — you own the databaseStored on Doist servers
Kanban / GanttBuilt-in, no upsellBoard view on Pro only
Natural language inputNot supported nativelyStrong ("every Monday at 9am")
IntegrationsREST API, webhooks, CalDAV100+ native integrations
Mobile app polishFunctional, actively improvedBest-in-class UX
Maintenance burdenYou handle updates and backupsZero
Multi-user / teamsUnlimited users on your instanceWorkspace plan needed for teams

The short version: Todoist wins on natural language date parsing and mobile UX refinement. Vikunja wins on cost at scale, Kanban/Gantt access without a paywall, and keeping project data off third-party infrastructure. If you are already running a home server and value data sovereignty, the setup cost is a one-time hour of work.

Keeping Vikunja Updated

cd /opt/vikunja
docker compose pull
docker compose up -d

Vikunja runs database migrations automatically on startup. Check the release notes before pulling a major version bump — breaking changes are rare but documented.

Backup

Two things to back up: the PostgreSQL database and the files volume.

# Dump the database
docker compose exec db pg_dump -U vikunja vikunja | gzip > vikunja_db_$(date +%F).sql.gz

# Archive file attachments
tar czf vikunja_files_$(date +%F).tar.gz ./files

Automate both with a systemd timer or cron job and ship the archives off-site (restic to Backblaze B2 is a common pairing).

Troubleshooting

Container exits immediately

Run docker compose logs vikunja. The most common cause is a missing or mistyped environment variable — particularly VIKUNJA_SERVICE_JWTSECRET being left as the placeholder.

Can't reach the login page

Verify the container is listening: curl -I http://localhost:3456. If that succeeds, the issue is in the reverse proxy config or a firewall rule. Check that port 443 is open:

# ufw
sudo ufw allow 443/tcp

# firewalld (Fedora/RHEL)
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Database connection refused

The healthcheck in the Compose file makes Vikunja wait for PostgreSQL to be ready. If you removed it, Vikunja may start before the database accepts connections. Add the healthcheck back and run docker compose up -d again.

Mobile app reports "invalid server URL"

Confirm TLS is valid (curl -v https://tasks.example.com/api/v1/info should return JSON). Self-signed certificates are rejected by the mobile apps; use a proper Let's Encrypt cert via Caddy or Certbot.

tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can I migrate my tasks from Todoist to Vikunja?
Vikunja can import from Todoist via its built-in migration tool found under account settings → Migrations. You'll need a Todoist API token. The import covers tasks, due dates, projects, and labels, though some metadata may not transfer perfectly.
Does Vikunja support CalDAV so I can sync with my calendar?
Yes. Vikunja exposes a CalDAV endpoint at /dav. You can add it to Thunderbird, Apple Calendar, or any CalDAV-compatible client to see tasks with due dates as calendar events.
How do I upgrade Vikunja without losing data?
Run docker compose pull followed by docker compose up -d. Vikunja applies database migrations automatically on startup. Always take a pg_dump backup before pulling a major version increment.
Is it safe to expose Vikunja directly on port 443 without a reverse proxy?
Vikunja's built-in server does not handle TLS natively, so a reverse proxy is required for HTTPS. Never expose port 3456 directly to the internet without TLS; credentials would travel in plaintext.
Can multiple people use the same instance with their own private tasks?
Yes. Every user's tasks are private by default; nothing is shared until a user explicitly shares a project or task. You can host an instance for a whole family or small team on modest hardware.

Related guides