Skip to Content

Production Guide: Deploy Vikunja with Docker Compose + Caddy + PostgreSQL on Ubuntu

Run Vikunja as a secure team task-management platform with HTTPS, PostgreSQL, backups, and practical operations checks.

Vikunja is a practical open-source task and project management platform for teams that want shared projects, recurring work, labels, reminders, and collaboration without placing every operational detail in a third-party SaaS tool. A common real-world use case is an IT or operations team that needs a private board for maintenance windows, onboarding checklists, recurring compliance tasks, and incident follow-up items. This guide deploys Vikunja on a single Ubuntu server with Docker Compose, PostgreSQL for durable data, and Caddy for automatic HTTPS.

The goal is not just to make the first page load. The deployment below separates application data from database data, binds the application only to localhost, keeps secrets out of the Compose file, adds basic security headers, and includes backup and verification routines. That makes the stack easier to operate after the excitement of the initial install has worn off.

Architecture and flow overview

The flow is intentionally simple: users connect over HTTPS to Caddy, Caddy terminates TLS and reverse proxies to Vikunja on 127.0.0.1:3456, and Vikunja stores structured data in PostgreSQL. Attachments and uploaded files stay in a host-mounted directory under /opt/vikunja/data. PostgreSQL data stays under /opt/vikunja/db. Only ports 80 and 443 are exposed publicly; the application and database are not directly reachable from the internet.

This single-host architecture is a good fit for small and mid-sized internal teams. If you later need high availability, the same operating model still helps: move PostgreSQL to a managed database, move file storage to durable object storage if your Vikunja version supports it, and run multiple application replicas behind a load balancer. Start with the simplest design you can monitor and restore.

Prerequisites

  • An Ubuntu 22.04 or 24.04 server with a non-root sudo user.
  • A DNS record such as tasks.example.com pointing to the server.
  • Ports 80 and 443 open from the public internet for Caddy certificate issuance.
  • At least 2 GB RAM for light usage; 4 GB is more comfortable for multiple teams and attachments.
  • A working SMTP provider if you want invitations, password resets, and notifications.

Step-by-step deployment

1) Install Docker, Compose, Caddy, and firewall basics

Install the container runtime, Caddy, and a minimal firewall first. Keep SSH open, then allow HTTP and HTTPS for certificate issuance and user traffic.

# Ubuntu 22.04/24.04 baseline
sudo apt update
sudo apt install -y ca-certificates curl gnupg ufw fail2ban
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
sudo apt install -y docker-compose-plugin caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

If the copy button is unavailable in your browser, manually select the command block above and copy it.

2) Create the application layout and strong secrets

Keep the stack in one predictable directory. The .env file should be readable only by the deployment user because it contains the PostgreSQL password and Vikunja JWT secret. Replace tasks.example.com with the real hostname before starting the stack.

sudo mkdir -p /opt/vikunja/{data,db,backups}
sudo chown -R $USER:$USER /opt/vikunja
cd /opt/vikunja
umask 077
openssl rand -hex 32 > .jwt_secret
openssl rand -hex 24 > .postgres_password
cat > .env <<'EOF'
VIKUNJA_DOMAIN=tasks.example.com
VIKUNJA_TIMEZONE=UTC
POSTGRES_DB=vikunja
POSTGRES_USER=vikunja
EOF
printf 'POSTGRES_PASSWORD=%s
' "$(cat .postgres_password)" >> .env
printf 'VIKUNJA_SERVICE_JWTSECRET=%s
' "$(cat .jwt_secret)" >> .env

If the copy button is unavailable in your browser, manually select the command block above and copy it.

3) Define the Docker Compose stack

The Compose file uses PostgreSQL 16 and the official Vikunja image. Notice that Vikunja is published only on 127.0.0.1. This is deliberate: Caddy is the only public entry point, so application traffic always passes through HTTPS and the headers you define at the edge.

cat > docker-compose.yml <<'EOF'
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 10

  vikunja:
    image: vikunja/vikunja:latest
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    env_file: .env
    environment:
      VIKUNJA_SERVICE_PUBLICURL: https://${VIKUNJA_DOMAIN}/
      VIKUNJA_SERVICE_JWTSECRET: ${VIKUNJA_SERVICE_JWTSECRET}
      VIKUNJA_DATABASE_TYPE: postgres
      VIKUNJA_DATABASE_HOST: postgres
      VIKUNJA_DATABASE_DATABASE: ${POSTGRES_DB}
      VIKUNJA_DATABASE_USER: ${POSTGRES_USER}
      VIKUNJA_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      VIKUNJA_DEFAULTSETTINGS_TIMEZONE: ${VIKUNJA_TIMEZONE}
    volumes:
      - ./data:/app/vikunja/files
    ports:
      - "127.0.0.1:3456:3456"
EOF

If the copy button is unavailable in your browser, manually select the command block above and copy it.

4) Configure Caddy for HTTPS

Caddy will request and renew certificates automatically once DNS points at the server. The Caddyfile also adds a few conservative headers that are safe for most deployments. If your base Caddyfile already imports sites-enabled/*.caddy, do not append the import line a second time.

sudo tee /etc/caddy/sites-available/vikunja.caddy >/dev/null <<'EOF'
tasks.example.com {
  encode zstd gzip
  reverse_proxy 127.0.0.1:3456
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
}
EOF
sudo mkdir -p /etc/caddy/sites-enabled
sudo ln -sf /etc/caddy/sites-available/vikunja.caddy /etc/caddy/sites-enabled/vikunja.caddy
printf '
import sites-enabled/*.caddy
' | sudo tee -a /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

If the copy button is unavailable in your browser, manually select the command block above and copy it.

5) Start Vikunja and check the first boot

Pull images, start the stack, and inspect the logs. On first boot Vikunja initializes its database schema. If the HTTPS request returns a login or setup page, create the first account and immediately disable open registration if your team requires invite-only access.

cd /opt/vikunja
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=80 vikunja
curl -I https://tasks.example.com

If the copy button is unavailable in your browser, manually select the command block above and copy it.

Configuration and secrets handling best practices

Do not paste production passwords directly into tutorials, tickets, or chat messages. Store them in .env with restrictive permissions, back up the file securely, and rotate credentials when administrators leave. If you use a central secrets manager, generate the .env file at deploy time instead of committing it to Git.

For email, add SMTP settings only after the web deployment is stable. Test password reset and invitation workflows with a non-admin account. If your organization uses SSO, place that integration in a change window and keep a local administrator account available until you have verified login, logout, and recovery flows.

Operationally, decide who can create teams, projects, and public shares. Vikunja can become messy if every user improvises a different taxonomy. Define labels for common work types, agree on naming conventions, and keep one administrative project for recurring maintenance tasks such as backup reviews and upgrade checks.

Backups and recovery routine

A task-management system is only useful if recovery is boring. Back up both PostgreSQL and uploaded files. Store at least one copy off the server, and occasionally restore into a temporary directory or staging host to prove that the files are usable.

sudo tee /usr/local/sbin/backup-vikunja >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/vikunja
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p backups
source .env
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "backups/vikunja-db-$stamp.sql.gz"
tar -czf "backups/vikunja-files-$stamp.tar.gz" data
find backups -type f -mtime +14 -delete
EOF
sudo chmod 750 /usr/local/sbin/backup-vikunja
sudo /usr/local/sbin/backup-vikunja
( sudo crontab -l 2>/dev/null; echo '17 2 * * * /usr/local/sbin/backup-vikunja >/var/log/backup-vikunja.log 2>&1' ) | sudo crontab -

If the copy button is unavailable in your browser, manually select the command block above and copy it.

The local retention example keeps 14 days. That is not a complete backup strategy by itself. Use object storage, another server, or your backup platform to copy the generated archives away from the host. Document the restore steps in the same repository or runbook where you track server maintenance.

Verification checklist

  • DNS: tasks.example.com resolves to the server.
  • TLS: the browser shows a valid certificate and no mixed-content warnings.
  • Database: PostgreSQL health checks are passing in docker compose ps.
  • Account flow: create a normal user, assign a project, and confirm permissions behave as expected.
  • Email: if SMTP is enabled, verify invitations and password resets.
  • Backups: run the backup script manually and confirm both database and file archives exist.
cd /opt/vikunja
docker compose exec -T postgres pg_isready -U vikunja -d vikunja
curl -fsS https://tasks.example.com/api/v1/info | jq .
docker compose logs --since=10m vikunja | tail -100
sudo journalctl -u caddy --since '10 minutes ago' --no-pager

If the copy button is unavailable in your browser, manually select the command block above and copy it.

Common issues and fixes

Caddy cannot issue a certificate

Check that DNS points to the correct public IP, ports 80 and 443 are reachable, and no other service is bound to those ports. Review journalctl -u caddy for the exact ACME error. Temporary DNS mistakes are common; fix the record, wait for propagation, then reload Caddy.

Vikunja starts but redirects to the wrong URL

Confirm VIKUNJA_SERVICE_PUBLICURL exactly matches the external HTTPS URL, including the trailing slash. After changing it, run docker compose up -d so the container receives the updated environment.

Database authentication fails

Make sure the PostgreSQL password in .env matches the password used when the database volume was first initialized. If you changed the password after first boot, update it inside PostgreSQL or rebuild from a clean volume during the initial setup phase.

Uploads fail or disappear

Check permissions on /opt/vikunja/data and confirm the volume is mounted into the container. Include this directory in backups; database-only backups will not preserve attachments.

Upgrades feel risky

Back up first, read the Vikunja release notes, pull images, and restart during a maintenance window. Keep the previous database and file archives until users have verified core workflows.

cd /opt/vikunja
cp .env .env.$(date -u +%Y%m%d)
docker compose pull
docker compose up -d
docker compose ps
sudo /usr/local/sbin/backup-vikunja

If the copy button is unavailable in your browser, manually select the command block above and copy it.

FAQ

Can I run Vikunja with SQLite instead of PostgreSQL?

SQLite is useful for quick tests, but PostgreSQL is the better production default. It handles concurrent users more predictably and fits standard backup and monitoring workflows.

Should PostgreSQL be exposed to the internet?

No. In this design PostgreSQL is reachable only from the Docker network. Do not publish the database port unless you have a specific private-network requirement and firewall controls.

How do I make the site invite-only?

Create the first administrator, review the registration settings in Vikunja, and disable open registration if the instance is for a private team. Test with a non-admin account before announcing the service.

What should I monitor first?

Monitor container restarts, disk space under /opt/vikunja, certificate renewal, backup job success, and HTTP availability. Those signals catch most early operational problems.

Can I put Vikunja behind Cloudflare or another proxy?

Yes, but keep Caddy as the origin reverse proxy and ensure the external URL, forwarded headers, and TLS mode are consistent. Avoid flexible TLS modes that create confusing redirects.

How often should I upgrade Vikunja?

Review releases monthly and patch sooner for security fixes. Always take a fresh backup before upgrades and keep a brief rollback note with the image tag you were running previously.

Does this replace a full project portfolio system?

Not always. Vikunja is excellent for team tasks and recurring operational work. Larger portfolio reporting, budgets, and executive dashboards may still belong in a separate system.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy ERPNext with Docker Compose + Caddy + MariaDB + Redis on Ubuntu
A production-oriented ERPNext deployment pattern with TLS, durable data, Redis queues, backup routines, and operational checks.