Vikunja is an open source task manager that combines to-do lists, Kanban boards, Gantt charts and CalDAV sync in one application, a self-hosted alternative to Todoist or Trello. In this tutorial you will run Vikunja and a MariaDB database with Docker Compose on Ubuntu 24.04, publish it behind Nginx with a free Let's Encrypt certificate, create your account, and connect a CalDAV client and the REST API.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with at least 1 GB of RAM, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository.
  • A domain name with an A record pointing to your server's IP address. This guide uses tasks.your_domain; replace it with your own hostname everywhere.
  • Ports 80 and 443 reachable from the internet.

Step 1 - Creating the project directory and secrets

Keep everything Vikunja needs under /opt/vikunja: the Compose file, a .env file with secrets, uploaded attachments and the database files.

sudo mkdir -p /opt/vikunja/files /opt/vikunja/db
cd /opt/vikunja

The Vikunja container runs as user ID 1000, so that user must be able to write to the attachments directory:

sudo chown 1000:1000 /opt/vikunja/files

Generate a random database password and a JWT secret (used to sign login sessions) and store them in /opt/vikunja/.env. Docker Compose reads this file automatically and substitutes the values into the Compose file:

sudo tee /opt/vikunja/.env > /dev/null <<EOF
DB_PASSWORD=$(openssl rand -hex 24)
DB_ROOT_PASSWORD=$(openssl rand -hex 24)
JWT_SECRET=$(openssl rand -hex 32)
EOF
sudo chmod 600 /opt/vikunja/.env

Check that the three values were written:

sudo cat /opt/vikunja/.env
DB_PASSWORD=5f0c1e9b7a...
DB_ROOT_PASSWORD=a41d93c2e8...
JWT_SECRET=0b7e6f2d91...

Step 2 - Writing the Docker Compose file

The current vikunja/vikunja image contains both the API and the web interface and listens on port 3456. Create the Compose file:

sudo nano /opt/vikunja/docker-compose.yml

Paste the following content:

services:
  vikunja:
    image: vikunja/vikunja:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:3456:3456"
    environment:
      VIKUNJA_SERVICE_PUBLICURL: https://tasks.your_domain/
      VIKUNJA_SERVICE_JWTSECRET: ${JWT_SECRET}
      VIKUNJA_DATABASE_TYPE: mysql
      VIKUNJA_DATABASE_HOST: db
      VIKUNJA_DATABASE_USER: vikunja
      VIKUNJA_DATABASE_PASSWORD: ${DB_PASSWORD}
      VIKUNJA_DATABASE_DATABASE: vikunja
    volumes:
      - ./files:/app/vikunja/files
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mariadb:11.4
    restart: unless-stopped
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_USER: vikunja
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_DATABASE: vikunja
    volumes:
      - ./db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 30s
      interval: 10s
      timeout: 5s
      retries: 5

A few details matter here:

  • The port is published only on 127.0.0.1. Ports published by Docker bypass UFW, so binding to localhost is what keeps Vikunja reachable only through Nginx.
  • VIKUNJA_SERVICE_PUBLICURL must be the exact HTTPS address users will open; Vikunja uses it to build links in emails and CalDAV URLs.
  • depends_on with service_healthy makes Vikunja wait until MariaDB accepts connections, which avoids a failed first start.

Validate the file. With --quiet, Compose prints nothing when the syntax is correct and the variables from .env resolve:

sudo docker compose config --quiet && echo "compose file OK"
compose file OK

Step 3 - Starting Vikunja

Pull the images and start both containers in the background:

sudo docker compose up -d

Wait about 30 seconds for MariaDB to initialize, then check the status:

sudo docker compose ps
NAME              IMAGE                    SERVICE   STATUS                    PORTS
vikunja-db-1      mariadb:11.4             db        Up 45 seconds (healthy)   3306/tcp
vikunja-vikunja-1 vikunja/vikunja:latest   vikunja   Up 14 seconds             127.0.0.1:3456->3456/tcp

Query the API info endpoint from the server itself. A JSON reply means Vikunja is running and connected to its database:

curl -s http://127.0.0.1:3456/api/v1/info | head -c 200; echo
{"version":"v0.24.6","frontend_url":"https://tasks.your_domain/","motd":"","link_sharing_enabled":true,...

If the command returns nothing, read the logs with sudo docker compose logs vikunja.

Step 4 - Configuring Nginx and HTTPS

Install Nginx and Certbot with its Nginx plugin:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Allow HTTP and HTTPS through UFW (keep SSH allowed if UFW is active):

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'

Create a server block for Vikunja:

sudo nano /etc/nginx/sites-available/vikunja
server {
    listen 80;
    listen [::]:80;
    server_name tasks.your_domain;

    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:3456;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

client_max_body_size raises Nginx's 1 MB default so task attachments can be uploaded. Enable the site, test the syntax and reload:

sudo ln -s /etc/nginx/sites-available/vikunja /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Request a certificate. Certbot edits the server block to add the TLS configuration and an HTTP to HTTPS redirect:

sudo certbot --nginx -d tasks.your_domain

When it finishes, open https://tasks.your_domain in your browser. You should see the Vikunja login page.

Step 5 - Creating your account and closing registration

Click Create account, enter a username, email and password, and log in. Vikunja creates a default Inbox project for new users.

Public registration is enabled by default, which means anyone who finds the URL can create an account on your server. Once you and your team have registered, turn it off by adding this line to the environment section of the vikunja service in /opt/vikunja/docker-compose.yml:

      VIKUNJA_SERVICE_ENABLEREGISTRATION: "false"

Recreate the container to apply the change:

cd /opt/vikunja
sudo docker compose up -d

Log out and reload the login page: the Create account link is gone.

Step 6 - Working with projects and Kanban boards

Create a project from New project in the sidebar, give it a name and optionally a color. Each project offers several views that you switch between at the top of the page: List, Gantt, Table and Kanban.

In the Kanban view, tasks live in buckets (columns). Rename the default buckets or add new ones such as "In review", and drag tasks between them. Opening a task lets you set a due date, priority, labels, assignees, reminders and attachments.

To work with other people, open the project menu and choose Share. You can share with individual users or teams and grant read only, read and write, or admin rights, or create a link share for people without an account.

Step 7 - Syncing tasks over CalDAV

Vikunja exposes projects as CalDAV task lists, so you can manage tasks from apps such as Thunderbird, Tasks.org on Android (through DAVx5) or the Reminders app on iOS.

For clients, it is better to use a dedicated CalDAV token than your account password. In Vikunja, open Settings > CalDAV, create a token and copy it; it is shown only once. The same page shows the CalDAV URL for your account, which has this form:

https://tasks.your_domain/dav/principals/your_username/

In your client, add a CalDAV account with that URL, your Vikunja username, and the token as the password. Test that the endpoint answers with the token before configuring the app:

curl -s -o /dev/null -w "%{http_code}\n" -X PROPFIND -u your_username:your_caldav_token https://tasks.your_domain/dav/principals/your_username/
207

A 207 Multi-Status reply means authentication works. A 401 means the username or token is wrong.

Step 8 - Automating tasks with the REST API

Vikunja's web interface is built on a documented REST API, which you can use from scripts. Create an API token in Settings > API Tokens, choose an expiry date and the permissions it needs (for example, reading and creating tasks), and copy the token.

Store it in a shell variable and list your projects (jq formats the JSON; install it with sudo apt install jq):

TOKEN="your_api_token"
curl -s -H "Authorization: Bearer $TOKEN" https://tasks.your_domain/api/v1/projects | jq '.[] | {id, title}'
{
  "id": 1,
  "title": "Inbox"
}

Create a task in project 1. Vikunja uses PUT to create objects:

curl -s -X PUT "https://tasks.your_domain/api/v1/projects/1/tasks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Renew TLS certificates", "priority": 3, "due_date": "2026-12-31T09:00:00Z"}' | jq '{id, title, done}'
{
  "id": 12,
  "title": "Renew TLS certificates",
  "done": false
}

The full API reference is served by your own instance at https://tasks.your_domain/api/v1/docs.

Step 9 - Backing up Vikunja

Your data lives in two places: the MariaDB database and the files directory. Dump the database with the credentials from .env and archive the attachments:

cd /opt/vikunja
sudo docker compose exec -T db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" vikunja' | gzip > ~/vikunja-db-$(date +%F).sql.gz
sudo tar -czf ~/vikunja-files-$(date +%F).tar.gz -C /opt/vikunja files

Check that both archives have a reasonable size:

ls -lh ~/vikunja-*

Copy them off the server, for example to object storage, on a regular schedule.

Troubleshooting

The Vikunja container restarts in a loop. Run sudo docker compose logs vikunja. Errors mentioning permission denied on /app/vikunja/files mean the directory is not owned by UID 1000; run sudo chown -R 1000:1000 /opt/vikunja/files. Database connection errors usually mean the password in .env changed after MariaDB was first initialized: the database keeps the original password.

Links in emails or CalDAV point to the wrong address. VIKUNJA_SERVICE_PUBLICURL must match the URL users open, including https:// and the trailing slash. Fix it and run sudo docker compose up -d.

Upload fails with "413 Request Entity Too Large". Increase client_max_body_size in /etc/nginx/sites-available/vikunja and reload Nginx.

Conclusion

You now have Vikunja running with Docker Compose and MariaDB, served over HTTPS by Nginx, with registration closed, CalDAV sync and API access. From here you can enable SMTP so reminders reach your inbox, connect an OpenID Connect provider for single sign-on, and schedule the backup commands from Step 9 with a systemd timer or cron job.