Vikunja is a free, open-source, self-hosted to-do and project app. Lists, Kanban boards, Gantt charts and a table view, with due dates, reminders, labels, sharing and teams. It is a solid replacement for Todoist, Trello or Microsoft To Do, and it has a CalDAV server, so your tasks can show up in other apps too.
In this guide we will run Vikunja in Docker using the official image with PostgreSQL, following the project's own full Docker example, and get it ready to sit behind a reverse proxy.
vikunja/vikunja. Since v0.22 the API and web frontend come in a single image.✅ Before you start
- A Linux host with Docker and Docker Compose v2 installed (
docker compose versionshould work). Usedocker composewith a space: the old Pythondocker-composev1 can crash on modern Docker. - One free port,
3456, for the web UI and API. - Optional: a reverse proxy such as Traefik or Nginx Proxy Manager and a domain, if you want to reach it from outside your home network.
STEP 1: User and group IDs (and your secrets)
Vikunja does not use PUID/PGID. It runs as user 1000 (with no group) and will not take ownership of its files folder by itself, so we hand that folder to user 1000 in STEP 2.
What you do need are two secrets: a database password and VIKUNJA_SERVICE_SECRET, which signs logins:
openssl rand -hex 24 # database password
openssl rand -hex 32 # VIKUNJA_SERVICE_SECRETKeep them somewhere safe, such as your password manager. In the examples below they appear as placeholders like CHANGE_ME_STRONG_PASSWORD and change-me.
STEP 2: Create the folders
I keep every app's files under one parent folder so a backup is a single job. Create the folders for Vikunja:
mkdir -p /path/to/appdata/vikunja/{files,db}
sudo chown 1000 /path/to/appdata/vikunja/filesIf you run Docker in rootless mode, the chown trick does not work; the docs suggest running the container with user: "0:0" instead, which is safe there.
When you are done, the layout should look like this:
/path/to/appdata/vikunja/
docker-compose.yml
files/ # task attachments and avatars (owned by user 1000)
db/ # PostgreSQLFolder layout
Finally, I give every web app that sits behind my reverse proxy one shared Docker network called proxy. Vikunja joins it so Traefik or Nginx Proxy Manager can reach it by container name, while its database and helpers stay on the stack's own private network. Create it once (if it already exists, Docker just tells you so):
docker network create proxySTEP 3: The compose file
Create /path/to/appdata/vikunja/docker-compose.yml and paste in the following. It is the official "example without any proxy", with my paths, a container name per service and the shared proxy network added. Use the copy button in the top right corner of the block.
services:
vikunja:
image: vikunja/vikunja:latest
container_name: vikunja
restart: unless-stopped
environment:
VIKUNJA_SERVICE_PUBLICURL: http://192.168.1.20:3456/ # later: https://tasks.example.com/
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: CHANGE_ME_STRONG_PASSWORD
VIKUNJA_DATABASE_TYPE: postgres
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_SECRET: change-me # the secret from STEP 1
VIKUNJA_SERVICE_TIMEZONE: Europe/London
# VIKUNJA_SERVICE_ENABLEREGISTRATION: "false" # uncomment after creating accounts
ports:
- "3456:3456"
volumes:
- /path/to/appdata/vikunja/files:/app/vikunja/files
depends_on:
db:
condition: service_healthy
networks:
- default
- proxy
db:
image: postgres:18
container_name: vikunja-db
restart: unless-stopped
environment:
POSTGRES_PASSWORD: CHANGE_ME_STRONG_PASSWORD
POSTGRES_USER: vikunja
POSTGRES_DB: vikunja
volumes:
- /path/to/appdata/vikunja/db:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"]
interval: 2s
start_period: 30s
networks:
proxy:
external: truedocker-compose.yml
STEP 4: The settings explained
| Setting | What it does |
|---|---|
VIKUNJA_SERVICE_PUBLICURL |
The address your browser uses to reach Vikunja, with a trailing slash. The web app sends its API calls there, so it must be reachable from your devices, not just the server. |
VIKUNJA_DATABASE_* |
How Vikunja reaches PostgreSQL: the db container, the vikunja user and database, and the password. It must match POSTGRES_PASSWORD. |
VIKUNJA_SERVICE_SECRET |
Signs login tokens. Long, random and stable; changing it logs everyone out. |
VIKUNJA_SERVICE_TIMEZONE |
Time zone for reminders and due dates. List of zones. |
VIKUNJA_SERVICE_ENABLEREGISTRATION |
Whether new users can sign up. Default true; set it to "false" once your accounts exist. |
/app/vikunja/files |
Attachments, avatars and background images. Must be writable by user 1000. |
postgres:18 |
The database. With Postgres 18, mount /var/lib/postgresql (not .../data), as in the official example. |
depends_on + healthcheck |
Vikunja waits until Postgres is accepting connections before it starts. |
Vikunja also runs on SQLite: drop the
db service and the database variables, mount /path/to/appdata/vikunja/db:/db (owned by user 1000) and Vikunja keeps its database in /db/vikunja.db. The docs recommend Postgres or MySQL for more than a handful of users.STEP 5: Start Vikunja
From the folder containing your compose file:
cd /path/to/appdata/vikunja
docker compose up -d
docker compose logs -fWait until the logs settle down (press Ctrl+C to stop following them; the container keeps running), then check it is up:
docker ps --filter name=vikunjaYou should see vikunja and vikunja-db listed as Up.
STEP 6: First-run setup
- Open
http://192.168.1.20:3456and click Register. Create your account (and accounts for anyone else who needs one). - Uncomment
VIKUNJA_SERVICE_ENABLEREGISTRATION: "false"and rundocker compose up -dso strangers cannot sign up. - Create a project and add a few tasks. Switch between the List, Kanban, Gantt and Table views at the top.
- Under Settings, set your language, week start and default reminder time, and import from Todoist, Trello or Microsoft To Do if you are moving over.
- Use Teams to share projects with the household.
STEP 7: Put Vikunja behind a reverse proxy
Use a dedicated hostname such as tasks.example.com, and change VIKUNJA_SERVICE_PUBLICURL to https://tasks.example.com/, otherwise the web app will keep trying to talk to the old address.
Option A: Traefik (Docker labels)
If Traefik runs on the same Docker host and is attached to the proxy network, add these labels to the vikunja service. They assume your HTTPS entry point is called websecure and your certificate resolver is called cloudflare; change them to match your Traefik config.
labels:
- traefik.enable=true
- traefik.http.routers.vikunja.rule=Host(`tasks.example.com`)
- traefik.http.routers.vikunja.entrypoints=websecure
- traefik.http.routers.vikunja.tls.certresolver=cloudflare
- traefik.http.services.vikunja.loadbalancer.server.port=3456
- traefik.docker.network=proxydocker-compose.yml (extra lines)
Option B: Traefik (file provider)
If Traefik runs on a different machine, add a router and service to your dynamic config file instead:
http:
routers:
vikunja:
rule: Host(`tasks.example.com`)
entryPoints:
- websecure
tls:
certResolver: cloudflare
service: vikunja
services:
vikunja:
loadBalancer:
servers:
- url: http://192.168.1.20:3456dynamic.yml
Option C: Nginx Proxy Manager
Add a Proxy Host for tasks.example.com with scheme http, forward hostname vikunja (if NPM is on the proxy network) or 192.168.1.20, and forward port 3456. Request a certificate on the SSL tab and tick Force SSL. If attachment uploads fail with 413, add client_max_body_size 20m; under the host's Advanced tab.
Vikunja is on two networks, so keep the traefik.docker.network=proxy label. Once everything goes through the proxy, you can remove the ports: section.
Updating Vikunja
The latest tag follows the newest stable release. Database migrations run automatically on start. To update:
cd /path/to/appdata/vikunja
docker compose pull
docker compose up -d
docker image prune -ffiles and db folders (or use pg_dump, as the backup docs describe). Prefer to choose when to upgrade? Pin a tag such as vikunja/vikunja:2.7.0.Troubleshooting
The page loads but you cannot log in, or it says it cannot reach the API. VIKUNJA_SERVICE_PUBLICURL does not match the address in your browser.
Permission denied writing to files. The folder is not owned by user 1000. Run sudo chown 1000 /path/to/appdata/vikunja/files. On rootless Docker, use user: "0:0"; vikunja doctor can spot this for you.
Vikunja never starts. It is waiting for the database health check. Check docker logs vikunja-db and that both passwords match.
port is already allocated. Change the left-hand side, for example "3457:3456", and update the public URL to match.
That's it! Your to-do list now lives on your own server. Now for the hard part: actually doing the tasks. ✅