Productivity & Home
Vikunja logo

How to install Vikunja with Docker

Vikunja cover: How to install Vikunja with Docker
Difficulty: BeginnerTime: 20 minutesTested with: Vikunja v2.7 (official image)

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.

💡
Note: This guide works with the current Vikunja v2.7 release (v2.7.0 at the time of writing) from 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 version should work). Use docker compose with a space: the old Python docker-compose v1 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_SECRET

Keep 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/files

If 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/                           # PostgreSQL

Folder 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 proxy

STEP 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: true

docker-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.
👍
Tip: Just for you?
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 -f

Wait 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=vikunja

You should see vikunja and vikunja-db listed as Up.

STEP 6: First-run setup

  1. Open http://192.168.1.20:3456 and click Register. Create your account (and accounts for anyone else who needs one).
  2. Uncomment VIKUNJA_SERVICE_ENABLEREGISTRATION: "false" and run docker compose up -d so strangers cannot sign up.
  3. Create a project and add a few tasks. Switch between the List, Kanban, Gantt and Table views at the top.
  4. 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.
  5. 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=proxy

docker-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:3456

dynamic.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 -f
⚠️
Warning: Read the release notes first and make a backup: stop the stack and copy both the files 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. ✅