Vikunja Todo Setup: Manage Tasks with Kanban and Gantt Charts in Docker

Organize your workflow. Deploy Vikunja, a powerful open-source to-do and project management tool, using Docker Compose on a VPS.

Vikunja Todo Setup: Manage Tasks with Kanban and Gantt Charts in Docker

Self-hosting a collaborative project management tool requires balancing application performance, feature completeness, and architectural simplicity. While options like Nextcloud offer task tracking, they are heavy PHP monoliths that struggle with real-time UI responsiveness. Vikunja offers a modern, high-performance alternative. Written in Go for the API and Vue 3 for the frontend SPA, it features fast page loads, real-time sync, and native support for complex views like Kanban boards, Gantt charts, table views, and CalDAV syncing.

This technical guide provides a step-by-step walkthrough for deploying Vikunja on a Virtual Private Server (VPS) using Docker Compose, configuring PostgreSQL for data persistence, Redis for caching/websockets, and Caddy as an automatic TLS reverse proxy.


1. Vikunja's Architecture: Decoupled vs. Unified

Understanding Vikunja's system design is key to configuring it correctly. Vikunja consists of two core layers: 1. The Backend API: A stateless Go daemon (vikunja-api) that exposes a REST API, communicates with the database and Redis cache, handles CalDAV sync requests, issues JWTs, sends mail alerts, and dispatches webhooks. 2. The Frontend SPA: A client-side Single Page Application (vikunja-frontend) built with Vue 3 and TypeScript. The frontend executes entirely in the client's browser and interacts with the API exclusively via HTTP requests.

graph TD
    User([Browser Client]) -->|HTTPS / WSS| ReverseProxy[Caddy / Nginx Reverse Proxy]
    ReverseProxy -->|Serve Static Assets| Frontend[Vue 3 SPA Assets]
    ReverseProxy -->|API Requests: 3456/api/v1| API[Go API Daemon]
    API -->|TCP 5432| DB[(PostgreSQL)]
    API -->|TCP 6379| Cache[(Redis Broker)]

Historically, self-hosting required deploying separate containers for vikunja-api and vikunja-frontend, and configuring a web server to serve the frontend assets while routing /api/v1 traffic to the backend API.

Modern deployments of Vikunja utilize the unified image (vikunja/vikunja). This image packages both the Go backend and the compiled Vue 3 static assets into a single container. Internally, the Go binary serves the frontend assets directly for root web requests and handles API requests on /api/v1 and CalDAV requests on /dav. This simplifies the Docker Compose configuration by removing the need for internal routing rules between frontend and backend containers.


2. Database Design & Task Modeling

Before writing configuration files, it is useful to look at how Vikunja models project structures in PostgreSQL:

  • Projects (projects): The root containers for tasks. Every project belongs to an owner or group, and can be nested hierarchically.
  • Tasks (tasks): The primary units of work, storing attributes such as title, description, due_date, start_date, and priority.
  • Buckets (buckets): Represent the vertical columns on a Kanban board.
  • Task Buckets (task_buckets): A join table mapping tasks to buckets. It contains a position float field. Vikunja uses a double-precision float index to order tasks within a column. When dragging and dropping a task, the frontend calculates a position value halfway between the task above it and the task below it (e.g., placing a task between position 1.0 and 2.0 sets it to 1.5). This allows instant updates without re-indexing the entire column in the database.
  • Task Relations (task_relations): Vikunja supports task dependencies (e.g., blocking, blocked-by, subtask, parent-task, predecessor, successor). These relationships are verified at the API database layer to prevent circular dependencies, and they populate the connections rendered in the Gantt chart view.

3. Production docker-compose.yml Configuration

Create a dedicated directory on your VPS, such as /opt/vikunja, and structure your deployment files inside it:

mkdir -p /opt/vikunja/data
cd /opt/vikunja
touch docker-compose.yml .env

Here is a production-grade docker-compose.yml file. It configures the unified Vikunja app, a PostgreSQL database, a Redis cache, and Caddy to handle automatic Let's Encrypt SSL/TLS certificates.

version: '3.8'

services:
  vikunja:
    image: vikunja/vikunja:latest
    container_name: vikunja_app
    restart: unless-stopped
    ports:
      - "3456:3456"
    environment:
      # Database connection parameters
      VIKUNJA_DATABASE_TYPE: postgres
      VIKUNJA_DATABASE_HOST: vikunja_db
      VIKUNJA_DATABASE_PORT: 5432
      VIKUNJA_DATABASE_USER: ${POSTGRES_USER}
      VIKUNJA_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      VIKUNJA_DATABASE_DATABASE: ${POSTGRES_DB}

      # Core application settings
      VIKUNJA_SERVICE_JWTSECRET: ${JWT_SECRET}
      VIKUNJA_SERVICE_PUBLICURL: https://${VIKUNJA_DOMAIN}/
      VIKUNJA_SERVICE_ENABLEREGISTRATION: "false" # Set to true to allow user sign-ups

      # Redis integration for websockets and performance caching
      VIKUNJA_REDIS_ENABLED: "true"
      VIKUNJA_REDIS_HOST: vikunja_redis:6379

      # Email notifications (SMTP Configuration)
      VIKUNJA_MAIL_ENABLED: "true"
      VIKUNJA_MAIL_HOST: ${SMTP_HOST}
      VIKUNJA_MAIL_PORT: ${SMTP_PORT}
      VIKUNJA_MAIL_USERNAME: ${SMTP_USER}
      VIKUNJA_MAIL_PASSWORD: ${SMTP_PASSWORD}
      VIKUNJA_MAIL_FROMADDRESS: ${SMTP_FROM_EMAIL}
      VIKUNJA_MAIL_SKIPTLSVERIFY: "false"

      # Files and attachments limits
      VIKUNJA_FILES_MAXSIZE: "50MB"
      VIKUNJA_FILES_PATH: /app/vikunja/files
    volumes:
      - ./data/files:/app/vikunja/files
    depends_on:
      vikunja_db:
        condition: service_healthy
      vikunja_redis:
        condition: service_started
    networks:
      - vikunja_net

  vikunja_db:
    image: postgres:15-alpine
    container_name: vikunja_db
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - vikunja_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  vikunja_redis:
    image: redis:7-alpine
    container_name: vikunja_redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - ./data/redis:/data
    networks:
      - vikunja_net

  caddy:
    image: caddy:2-alpine
    container_name: vikunja_proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data/caddy_data:/data
      - ./data/caddy_config:/config
    depends_on:
      - vikunja
    networks:
      - vikunja_net

networks:
  vikunja_net:
    driver: bridge

4. Configuring Environment Variables & Caddy

The Environment File (.env)

Generate a cryptographically secure random string for your JWT_SECRET key to secure API authentication tokens:

openssl rand -hex 32

Use the output to populate your .env file, substituting the placeholder values below with your specific setup:

# Domain configuration
VIKUNJA_DOMAIN=tasks.example.com

# PostgreSQL Credentials
POSTGRES_USER=vikunjapg
POSTGRES_PASSWORD=Use_A_Secure_Password_Here
POSTGRES_DB=vikunja

# JWT Encryption Secret
JWT_SECRET=Insert_Output_From_OpenSSL_Here

# SMTP Mail Server Configuration
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=postmaster@example.com
SMTP_PASSWORD=YourSmtpPassword
SMTP_FROM_EMAIL=vikunja@example.com

The Caddyfile

Caddy acts as the front-facing web server, handling SSL generation via Let's Encrypt and proxying client requests to the unified Vikunja application container.

Create a file named Caddyfile in the /opt/vikunja directory:

tasks.example.com {
    # Reverse proxy all requests to the Vikunja container
    reverse_proxy vikunja:3456

    # Optional headers to enhance security
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-XSS-Protection "1; mode=block"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "no-referrer-when-downgrade"
    }

    # Enable Gzip compression to optimize asset loading speeds
    encode gzip
}

Replace tasks.example.com with the fully qualified domain name (FQDN) pointed at your VPS public IP address.


5. Deployment Step-by-Step Walkthrough

Follow these commands to deploy the stack:

Step 1: Set Volume Directory Ownership

Before launching, ensure the local mount directories exist and have permissions that match Docker container users. PostgreSQL and Redis handle their own directory permissions, but Vikunja needs read/write access to its local file uploads folder:

mkdir -p data/files data/postgres data/redis data/caddy_data data/caddy_config
chown -R 1000:1000 data/files

Step 2: Spin Up the Containers

Run the Docker Compose command to build and launch the services in the background:

docker compose up -d

Step 3: Inspect Logs and Migration Process

Vikunja features an automatic database migration system that runs on startup. Check the initialization logs to verify that the table structures were generated without error:

docker compose logs -f vikunja

Look for lines indicating database migrations were executed:

2026-05-30T08:50:00Z: INFO ▶ [Database] Migration 20230305141322 succeeded
2026-05-30T08:50:01Z: INFO ▶ [Database] All migrations run successfully.
2026-05-30T08:50:01Z: INFO ▶ [Server] Web server started on port :3456

Step 4: Admin Account Setup

Because we set VIKUNJA_SERVICE_ENABLEREGISTRATION: "false" in our compose file for security, you can register the initial admin account via the Vikunja Command Line Interface (CLI) inside the running application container:

docker compose exec vikunja /app/vikunja/vikunja user create --username admin --email admin@example.com --password "SecureAdminPassword"

Once completed, navigate to https://tasks.example.com in your browser. Log in with the credentials generated above.


6. Verification: Testing Kanban, Gantt, and CalDAV

Kanban Board View

To verify drag-and-drop mechanics: 1. Create a new Project. 2. In the project's view options, select Kanban. 3. Create columns (e.g., "To Do", "In Progress", "Done"). 4. Add a task and drag it across columns. 5. Open your browser's developer console (F12) to inspect network payloads. You will observe POST and PUT payloads targeting /api/v1/projects/:id/views/kanban detailing the position changes.

Gantt Chart View

To verify relationship mapping: 1. Select the Gantt view option. 2. Create two tasks: "Setup Server" and "Install App". 3. Edit the details of "Install App", select Relations, and set it as blocked by "Setup Server". 4. Set a start and end date for both tasks. 5. You will see a visual relation line connecting the tasks in the Gantt grid, reading from the task_relations table.

CalDAV Integration

To sync Vikunja tasks with desktop or mobile calendar clients (e.g., Thunderbird, Apple Reminders): 1. Navigate to user settings, click CalDAV, and copy the principal URL. 2. In Thunderbird (with the Lightning extension) or Apple Calendar, create a new network calendar. 3. Choose the CalDAV protocol, input the principal URL, and authenticate using your Vikunja username and password (or generated API token). 4. Tasks added in Vikunja will now populate your external calendar applications in real time.


7. Operational Maintenance & Backups

To secure production data, schedule nightly database dumps and file volume backups. Below is an automated backup script /opt/vikunja/backup.sh:

#!/bin/bash
BACKUP_DIR="/opt/vikunja/backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
mkdir -p "$BACKUP_DIR"

# Dump PostgreSQL database
docker compose exec -t vikunja_db pg_dump -U vikunjapg -d vikunja > "$BACKUP_DIR/db_backup_$TIMESTAMP.sql"

# Archive user uploaded files
tar -czf "$BACKUP_DIR/files_backup_$TIMESTAMP.tar.gz" -C /opt/vikunja/data files

# Keep only the last 7 days of backups
find "$BACKUP_DIR" -type f -mtime +7 -delete

Make it executable and register a cron job to execute it nightly:

chmod +x /opt/vikunja/backup.sh
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/vikunja/backup.sh") | crontab -

This ensures database snapshots and file attachments are stored safely, minimizing data-loss risks in production setups.