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.
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
trueand 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.
Step 3 — Reverse Proxy with Caddy (Recommended)
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:
- Open the app and tap Use a custom server.
- Enter your full URL:
https://tasks.example.com. Include the trailing path if you deployed under a subpath. - 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
| Feature | Vikunja (self-hosted) | Todoist (SaaS) |
|---|---|---|
| Cost | Free (hosting costs only) | Free tier; Pro ~$4/mo |
| Data ownership | Complete — you own the database | Stored on Doist servers |
| Kanban / Gantt | Built-in, no upsell | Board view on Pro only |
| Natural language input | Not supported natively | Strong ("every Monday at 9am") |
| Integrations | REST API, webhooks, CalDAV | 100+ native integrations |
| Mobile app polish | Functional, actively improved | Best-in-class UX |
| Maintenance burden | You handle updates and backups | Zero |
| Multi-user / teams | Unlimited users on your instance | Workspace 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.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.