Skip to main content

Vikunja

Deploy Vikunja task management server.

Ansible hosts group: vikunja​

Variables​

OptionTypeDescriptionDefault
cs_vikunja_docker_imagestringVikunja docker image{{ cs_vm_artifact_registry_containers_home }}/vikunja
cs_vikunja_docker_tagstringVikunja docker tag2.7.0
cs_vikunja_container_namestringContainer namevikunja
cs_vikunja_groupstringDedicated groupvikunja
cs_vikunja_userstringDedicated user (no home, no login)vikunja
cs_vikunja_user_gidintGID for the Vikunja group1990
cs_vikunja_user_uidintUID for the Vikunja user1991
cs_vikunja_server_dnslistDNS serversThe cluster's DNS servers (from Vault)
cs_vikunja_clusterstringCluster name{{ cs_cluster_name }}
cs_vikunja_timezonestringContainer timezoneAsia/Kolkata
cs_vikunja_container_rootstringSingle root directory: everything Vikunja owns/app/vikunja-container-root
cs_vikunja_files_dirstringHost directory for uploads, mounted at /files{{ cs_vikunja_container_root }}/files
cs_vikunja_cert_dirstringHost directory for certs, mounted at /certs{{ cs_vikunja_container_root }}/certs
cs_vikunja_public_uristringPublic URI for Vikunjahttps://vikunja-{{ inventory_hostname }}.<cluster domain>
cs_vikunja_extra_cors_originslistExtra origins to add to Vikunja's CORS origins[]
cs_vikunja_mailer_force_sslboolUse implicit TLS against the shared SMTP endpointtrue
cs_vikunja_cors_maxageintSeconds a CORS preflight response may be cached3600
cs_vikunja_trusted_proxieslistCIDRs trusted to set X-Forwarded-ForThe cluster CIDR and VPN CIDR (from Vault)
cs_vikunja_db_cluster_namestringDatabase cluster name{{ cs_vikunja_cluster }}
cs_vikunja_db_cluster_nodestringDatabase cluster node{{ inventory_hostname }}
cs_vikunja_db_databasestringDatabase namevikunja-{{ inventory_hostname }}
cs_vikunja_restic_cluster_namestringRestic cluster name{{ cs_vikunja_cluster }}
cs_vikunja_restic_node_namestringRestic backup node name{{ inventory_hostname }}
cs_vikunja_restic_repo_namestringRestic repository namevikunja

cs_vikunja_files_dir and cs_vikunja_cert_dir are host-side paths already nested under cs_vikunja_container_root, so a single Restic call over cs_vikunja_container_root captures everything Vikunja owns: config, uploaded files, and certificates alike. They are bind mounted into the container at the fixed in-container paths /files and /certs (VIKUNJA_FILES_BASEPATH and the database.sslcert / database.sslkey / database.sslrootcert paths point at /certs/... accordingly).

Database​

Vikunja connects to PostgreSQL over mandatory mutual TLS (verify-full), following the same contract as PostgreSQL:

  • The database name and the login username are both vikunja-{{ inventory_hostname }}.
  • Before the first install, apps_dict["vikunja-<hostname>"] = {user: "vikunja-<hostname>", password: "…"} must exist in the Postgres host's {{ cs_project_code }}/application-deployer/clusters/{{ cs_vikunja_db_cluster_name }}/hosts/{{ cs_vikunja_db_cluster_node }}/apps/postgresql/config secret: see PostgreSQL | Vault configurations.
  • --tags vikunja_prepare_db provisions the Vikunja PostgreSQL user and database through the PostgreSQL maintenance connection. See Prepare PostgreSQL Database for the shared process.
  • The install task issues a client certificate (CN = the DB login user) signed by the internal Root CA and connects with database.sslmode: verify-full, pointing database.sslcert / database.sslkey / database.sslrootcert at the mounted certificate directory. Vikunja has no database.port option: the port is embedded in database.host as host:port.
  • --tags vikunja_restic_restore imports the database dump restored from the latest Restic snapshot through the shared task: see Restore PostgreSQL Database for the process.

Email​

Vikunja sends outbound email for password resets, task reminders, and team invites. It reuses the single global SMTP secret from Vault (managed-services/smtp, keys smtphost / smtpport / smtpusername / smtppassword / fromaddress). The sender address is tagged with a plus-address so replies/bounces can be told apart per cluster/host: <fromaddress-local-part>+vikunja-{{ cs_vikunja_cluster }}-{{ inventory_hostname }}@<fromaddress-domain>. cs_vikunja_mailer_force_ssl selects implicit TLS against the SMTP endpoint; set it to false if the endpoint is STARTTLS-only instead.

Reverse proxy and TLS​

Vikunja serves plain HTTP on the port from its Vault config; the existing Nginx Proxy Manager terminates TLS in front of it. Vikunja's only built-in TLS mode is Let's Encrypt autotls, which cannot load an internally-signed certificate, so no server certificate is issued for it from the internal Root CA: only the mandatory Postgres client certificate uses the Root CA.

CORS and client IP extraction​

Vikunja's API is reachable both through Nginx Proxy Manager's public domain (HTTPS, cs_vikunja_extra_cors_origins on hosts where cs_vikunja_public_uri is overridden to an internal address, otherwise cs_vikunja_public_uri itself) and directly against the host's own address (http://<host-ip-or-hostname>:<port>, with <port> taken from the port key of Vikunja's Vault config and no reverse proxy in front, e.g. from the LAN/VPN). The install task (tasks/vikunja/main.yml) configures Vikunja so both paths work:

  • VIKUNJA_CORS_ENABLE / VIKUNJA_CORS_ORIGINS / VIKUNJA_CORS_MAXAGE (service.cors.*): CORS is enabled with an origins list built at task-run time from cs_vikunja_public_uri, plus cs_vikunja_extra_cors_origins, plus an http://<address>:<port> origin for every one of the host's IPv4 addresses, its short hostname, and its FQDN (ansible_facts.all_ipv4_addresses / .hostname / .fqdn), covering every way this host might be reached. cs_vikunja_extra_cors_origins exists for hosts where cs_vikunja_public_uri is overridden to an internal address, so the real public reverse-proxy domain is still a valid CORS origin. The list is joined with spaces, not commas: Vikunja reads VIKUNJA_CORS_ORIGINS via viper's GetStringSlice, which for an env-sourced string falls back to Go's cast.ToStringSliceE; for a plain string that's strings.Fields(v), splitting on whitespace. A comma-joined value is parsed as a single origin, so only cs_vikunja_public_uri would match (Vikunja re-appends it separately in code) and every other origin in the list would silently fail CORS checks.
  • VIKUNJA_SERVICE_IPEXTRACTIONMETHOD: xff with VIKUNJA_SERVICE_TRUSTEDPROXIES set to cs_vikunja_trusted_proxies: X-Forwarded-For is only trusted from that CIDR list (the range Nginx Proxy Manager itself lives in), so a request proxied through it resolves to the real client IP, while a request that reaches Vikunja directly (bypassing the proxy) still resolves safely to its own TCP connection address instead of a client-supplied header.

Users​

Self-service registration is disabled (VIKUNJA_SERVICE_ENABLEREGISTRATION: "false", hard-coded in tasks/vikunja/main.yml). Users are managed entirely through the Vikunja CLI (docs), run via community.docker.docker_container_exec against the running container by tasks/vikunja/users.yml (vikunja_users tag), which loops over every entry in the Vault users mapping, keyed by username, several times:

  • user create (tasks/vikunja/users.yml): creates the user with a one-off random password generated in-task (community.general.random_string) that is never persisted; it only needs to satisfy Vikunja's create-time password policy, since the real password (if any) is applied in the next step. A user that already exists is treated as present and does not fail the task, whether it is currently active ('User with that username already exists') or disabled by the last loop below ('Account is disabled'), either way the account gets re-enabled by user change-status --enable further down in the same run.
  • Further loops over the same mapping, each identifying the account by username (tasks/vikunja/users.yml):
    • user change-status --enable: ensures the account is enabled on every run.
    • user update --email: keeps the account's email in sync with Vault on every run.
    • user reset-password --direct --password: only when the Vault entry has a password field.
    • user set-admin: the task exists but is switched off (when: false), because the admin-panel license feature is not active, so the is_admin field has no effect and Vikunja's admin flags are left unchanged.

After those loops, tasks/vikunja/users.yml runs user list against the container and parses its table output (matching each │ <id> │ <username> │ ... row) to get the full set of usernames that actually exist in Vikunja. Any username in that set that is not a key in the Vault users mapping is disabled with user change-status --disable, so an account removed from Vault gets locked out on the next run instead of staying active indefinitely.

Vault configurations​

  • key: {{ cs_project_code }}/application-deployer/clusters/{{ cs_vikunja_cluster }}/hosts/{{ inventory_hostname }}/apps/vikunja/config
{
"port": "Web UI/API listen port",
"service_secret": "Random secret used to sign JWTs (service.secret)",
"users": {
"username": {
"email": "User email address",
"password": "User password",
"is_admin": "Administrator flag carried with the user configuration (not applied while the set-admin step is switched off)"
}
}
}

Backup​

Restic backs up the whole cs_vikunja_container_root directory (which includes files, certs, and the dumped db_backup.sql). The {{ cs_vikunja_container_name }} container is stopped for the duration of the backup, a fresh database dump is exported into cs_vikunja_container_root/db_backup.sql over mTLS, and an always block (which runs even if the backup itself fails) removes the temporary database dump and then starts the container again.

Cleanup (dangerously_cleanup_vikunja)​

Destructive. Only runs when this exact tag is passed explicitly (it is intentionally excluded from the plain vikunja tag) and permanently deletes Vikunja's own data on the host:

  • Removes the {{ cs_vikunja_container_name }} Docker container.
  • Deletes the UFW allow rule opened for the web UI/API port.
  • Deletes the Vikunja PostgreSQL database ({{ cs_vikunja_db_database }}) via the PostgreSQL maintenance connection.
  • Deletes cs_vikunja_container_root (including files and certificates).
  • Deletes the {{ cs_vikunja_user }} user, its home directory, and the {{ cs_vikunja_group }} group.

Tags​

  • vikunja: Deploy Vikunja.
  • vikunja_prepare_db: Provision the Vikunja PostgreSQL user and database.
  • vikunja_install: Install Vikunja.
  • vikunja_users: Create configured Vikunja users.
  • vikunja_restic_backup: Run Restic backup for Vikunja.
  • vikunja_restic_restore: Restore Vikunja's data from the latest Restic snapshot.
  • dangerously_cleanup_vikunja: Destructive. Removes the Vikunja Docker container, closes its UFW port, drops its PostgreSQL database, deletes cs_vikunja_container_root, and deletes the cs_vikunja_user user, its home directory, and the cs_vikunja_group group. Never included by the plain vikunja tag.

Deployment​

uv sync --all-extras --all-packages --no-progress
uv --offline run --no-sync --no-progress ansible-galaxy install -r requirements.yml
uv --offline run --no-sync --no-progress ansible-playbook playbook.yml --tags vikunja