Use case · Task management

Vikunja self hosted on a dedicated server, built from the official docs

Self-hosting Vikunja means running one container or one binary that serves both the API and the web frontend on port 3456, plus a database, a reverse proxy and a signing secret you choose. This page builds the Docker Compose version with PostgreSQL on a whole machine: the port bound to loopback, the first account created through an SSH tunnel, registration closed before the site goes public, and a nightly vikunja dump archive.

See the machines

What a Vikunja install consists of

Vikunja has two halves, the API and the frontend, and both are bundled into a single deployable binary or Docker image. Install routes in the official docs: a GPG-signed zip for the binary, apt, dnf, pacman and apk repositories (from release 2.4.0), and the vikunja/vikunja image, also published at ghcr.io/go-vikunja/vikunja. Images from 2.7.1 onwards are signed with cosign and carry a build provenance attestation.

  • The container runs as user 1000 with no group. The files directory must be writable by that user, and Vikunja will not chown it for you.
  • There is no default user or password. Whoever opens the site registers an account, which is why registration needs attention before the proxy goes live.
  • The docs publish no CPU or memory figures. Their sizing advice is about head count: beyond a handful of users, use MySQL or PostgreSQL instead of SQLite, and Redis caching only becomes worth adding at that point too.

Choosing the database

Vikunja speaks to SQLite, PostgreSQL, MySQL and MariaDB. The official CI tests MySQL 8, MariaDB 12, PostgreSQL 18 and the bundled SQLite; older releases may work but are untested, and the project recommends a currently maintained version.

EngineOldest version listedBackup method in the docsNotes
SQLiteBundled in the binary and imageCopy the database fileDefault type; the image keeps it at /db/vikunja.db
PostgreSQL9.5 on the install page, 12+ in the config referencepg_dumpUsed by every full Docker example
MySQL8.0; 5.7 lacks recursive CTEs and window functionsmysqldumpDatabase must be UTF-8 compatible
MariaDB10.2mysqldumpSet the database type to mysql

This build uses PostgreSQL 18, matching the full Docker example. SQLite suits a personal list; for a team, the docs point to PostgreSQL or MySQL.

Configuration: config.yml or environment

Every option lives in a nested config.yml, and almost all of them can also be set as environment variables: service.publicurl becomes VIKUNJA_SERVICE_PUBLICURL. When both are set, the variable wins. Any value can also be read from a file by adding _FILE to the variable name. In the official image, VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/ and VIKUNJA_DATABASE_PATH=/db/vikunja.db are preset, so a database.path in a mounted config file changes nothing.

  • service.secret signs the JWT session tokens. Unset, Vikunja generates a random one at every startup, so a restart invalidates every login. Older guides call it service.JWTSecret; that key is deprecated and its value is copied to service.secret.
  • service.publicurl is the URL users reach. It is used in emails and between frontend and API, and it is required while cors.enable is true, which is the default.
  • service.enableregistration defaults to true: anyone who can load the page can create an account.
  • service.interface defaults to :3456. files.maxsize defaults to 20MB, and the proxy body limit has to match it.

Which machine to put it on

A task manager alone will not stretch a whole server, so pick the class by what else shares the box. The dedicated server range groups lines by platform: Moonshot cartridges are sold as one service per machine, a single-socket server is described as a web or application host with its database alongside, and dual-socket machines carry many services side by side, for example Vikunja next to Plane for issue tracking and Joplin Server for notes.

  • Root on hardware with no hypervisor and no other tenant, and Linux images reinstalled from the control panel.
  • Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC). Among cartridges, only the m510 and m710x carry one; the rest get power control. That console is what gets you back in after a bad ufw rule.
  • A racked machine is handed over in about 30 minutes once payment is approved; built-to-order lines take 4 to 24 hours.
  • The network is our own autonomous system, AS19624, and billing runs monthly with no contract. Running other containers on the same metal is covered on containers on bare metal.

Building the stack with Docker Compose

Start from Ubuntu with Docker Engine (version 28 or later) and Compose v2, and point a hostname like tasks.example.com at the box. The firewall goes on first, with IPv6 enabled in ufw before any allow rule so each rule gets its IPv6 copy. Ports 80 and 443 are opened now for the nginx that runs on the host later.

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw --force enable
mkdir -p /opt/vikunja/files /opt/vikunja/db /opt/vikunja/backups
cd /opt/vikunja
chown 1000 files backups
chmod 700 backups
umask 077
DBPASS=$(openssl rand -hex 24)
printf 'POSTGRES_USER=vikunja\nPOSTGRES_PASSWORD=%s\n' "$DBPASS" > db.env
printf 'VIKUNJA_SERVICE_SECRET=%s\nVIKUNJA_DATABASE_PASSWORD=%s\n' "$(openssl rand -hex 32)" "$DBPASS" > vikunja.env
cat >> vikunja.env <<'EOF'
VIKUNJA_SERVICE_PUBLICURL=http://localhost:3456/
VIKUNJA_SERVICE_ENABLEREGISTRATION=true
VIKUNJA_DATABASE_TYPE=postgres
VIKUNJA_DATABASE_HOST=db
VIKUNJA_DATABASE_USER=vikunja
VIKUNJA_DATABASE_DATABASE=vikunja
EOF
umask 022
cat > compose.yaml <<'EOF'
services:
  vikunja:
    image: vikunja/vikunja:2.7.1
    env_file: vikunja.env
    ports:
      - "127.0.0.1:3456:3456"
    volumes:
      - ./files:/app/vikunja/files
      - ./backups:/backups
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
  db:
    image: postgres:18
    env_file: db.env
    volumes:
      - ./db:/var/lib/postgresql
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"]
      interval: 30s
      start_period: 15s
      start_interval: 5s
EOF
docker compose up -d

printf is a shell builtin, so neither secret appears in ps output or shell history, and umask 077 leaves both env files at mode 600. Compose reads them as root and hands the values to the containers. The image tag is pinned, so a pull never upgrades you by surprise.

Publish a port without an address and Docker listens on every host address, IPv4 0.0.0.0 and IPv6 [::]. For IPv4, published traffic bypasses ufw. For IPv6 it bypasses ufw only if Docker manages IPv6 networking itself; otherwise IPv6 connections reach docker-proxy, a userland process that ufw can filter. Binding to 127.0.0.1 avoids the question, and on Docker Engine 28 or newer a 127.0.0.1-only port is reachable from the host alone (moby#45610).

First account over SSH, then the public proxy

This order is our own suggestion, not a recipe from the docs. It works because the browser reaches the API at whatever publicurl says. Registration starts on and publicurl points at http://localhost:3456/, the address your browser will use through a tunnel. From your workstation, forward the port with your own login: ssh -L 3456:127.0.0.1:3456 you@your-server. Open http://localhost:3456, register your account and your colleagues' accounts, then switch the URL to the public name, close registration and put nginx with a Let's Encrypt certificate in front.

cd /opt/vikunja
sed -i -e 's|^VIKUNJA_SERVICE_PUBLICURL=.*|VIKUNJA_SERVICE_PUBLICURL=https://tasks.example.com/|' \
  -e 's/^VIKUNJA_SERVICE_ENABLEREGISTRATION=.*/VIKUNJA_SERVICE_ENABLEREGISTRATION=false/' vikunja.env
docker compose up -d
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/vikunja <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name tasks.example.com;
    location / {
        proxy_pass http://127.0.0.1:3456;
        client_max_body_size 20M;
    }
}
EOF
ln -s /etc/nginx/sites-available/vikunja /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d tasks.example.com

If you would rather never open registration at all, set it to false from the start and create users from the CLI: docker compose exec vikunja /app/vikunja/vikunja user create -u alex -e alex@example.com. Leave out -p; Vikunja then asks for the password, and it stays out of ps and your history. nginx here is a normal host process, so it needs the 80/443 ufw rule on IPv4 and IPv6 alike; that rule is already in place.

Backups, restores and upgrades

Two things hold your data: the database and the attachments folder. Deleting a project is permanent, with no trash or undo, so the docs ask for a backup routine with sensible retention. vikunja dump writes one zip with the version, every stored file, the whole database and the config file if one is in use. This build has no config file, so its settings travel in the .env described next. According to the dump code in the Vikunja source (pkg/modules/dump/dump.go), it also stores every VIKUNJA_ environment variable in a .env inside the zip, so each archive holds the signing secret and the database password. The same code creates the zip with mode 0600.

Contents of the cron file, written with printf (output goes to a log file whose directory exists before the first run):

mkdir -m 700 -p /var/log/vikunja-dump
printf '%s\n' \
  'SHELL=/bin/sh' \
  '20 3 * * * root umask 077; cd /opt/vikunja && docker compose exec -T vikunja /app/vikunja/vikunja dump -p /backups > /var/log/vikunja-dump/dump-$(date +\%F).log 2>&1' \
  '50 3 * * * root find /opt/vikunja/backups -name "vikunja-dump_*.zip" -mtime +14 -delete' \
  > /etc/cron.d/vikunja-dump
chmod 644 /etc/cron.d/vikunja-dump

The 14-day retention is an example; pick yours. Copy the zips off the machine yourself, with rsync or scp from another host, since a zip sitting next to the data disappears with the same failed disk array. vikunja restore, given the archive's path, reads it back, and --preserve-config keeps the current settings instead of the dumped ones. Rehearse a restore on a scratch instance before you need it.

To upgrade: take a dump, read the changelog for manual steps, change the image tag in compose.yaml, docker compose pull stages the new image; docker compose up -d starts it. Vikunja runs any pending database migrations itself when it starts. Moving from a stable release to an unstable build works; moving back is not recommended, so wait for the next stable release instead.

Licence, Pro and the limits

Most of the repository is licensed AGPL-3.0-or-later. Run it unchanged for your team and nothing is owed; if you modify it and offer it to users over a network, the AGPL obliges you to offer them your modified source. Vikunja Pro is an optional licence key that unlocks an admin panel, time tracking and audit logs. With no key set, the instance makes no licence requests. With one, it contacts console.vikunja.io or check.vikunja.io at startup and once a day, sending the key, a random instance ID, the version, the database type, the host OS, a container flag and user counts. No project, task or user content is sent.

  • The mobile app covers only the most basic features, according to the install docs; the web frontend and desktop app are the full client.
  • You run the stack: patching the OS, Docker and Vikunja, and testing restores, stay with you.

Questions

Is SQLite enough for a self-hosted Vikunja?

For yourself or a small household, yes: SQLite ships inside the binary and image and needs nothing installed. The docs recommend MySQL or PostgreSQL once more than a handful of people use the instance. Decide before your team starts adding tasks.

Why does everyone get logged out when Vikunja restarts?

Because service.secret was never set. Without it, Vikunja generates a fresh random secret at each startup, and tokens signed with the old one stop working. Set VIKUNJA_SERVICE_SECRET (or service.secret in config.yml) to a long random value and keep it in a file at mode 600.

Which port does Vikunja listen on?

Port 3456, set by service.interface (default :3456). Behind a reverse proxy, publish it on 127.0.0.1 only and let nginx or Caddy take 80 and 443.

Can I run Vikunja without Docker?

Yes. Download the signed zip, unpack it to /opt/vikunja and use the systemd unit from the docs, or install the deb, rpm, Arch or Alpine package. The packaged unit runs as root; the systemd hardening guide moves it to a dedicated vikunja user with its own data directory.

Is there a built-in admin account?

No. A fresh install has no users and no default password, so you register or create the first account yourself. The instance-wide admin panel is a Vikunja Pro feature; without Pro, users are managed with the vikunja user CLI commands.

Put your task server on your own metal

Pick a dedicated server line by what will run beside Vikunja, with root, an unmetered port and month-to-month terms.

See the machines

Related on Server Room

Other use cases