Vikunja
Deploy Vikunja task management server.
Ansible hosts group: vikunja
Variables
| Option | Type | Description | Default |
|---|---|---|---|
cs_vikunja_docker_image | string | Vikunja docker image | {{ cs_vm_artifact_registry_containers_home }}/vikunja |
cs_vikunja_docker_tag | string | Vikunja docker tag | 2.7.0 |
cs_vikunja_container_name | string | Container name | vikunja |
cs_vikunja_group | string | Dedicated group | vikunja |
cs_vikunja_user | string | Dedicated user (no home, no login) | vikunja |
cs_vikunja_user_gid | int | GID for the Vikunja group | 1990 |
cs_vikunja_user_uid | int | UID for the Vikunja user | 1991 |
cs_vikunja_server_dns | list | DNS servers | The cluster's DNS servers (from Vault) |
cs_vikunja_cluster | string | Cluster name | {{ cs_cluster_name }} |
cs_vikunja_timezone | string | Container timezone | Asia/Kolkata |
cs_vikunja_container_root | string | Single root directory: everything Vikunja owns | /app/vikunja-container-root |
cs_vikunja_files_dir | string | Host directory for uploads, mounted at /files | {{ cs_vikunja_container_root }}/files |
cs_vikunja_cert_dir | string | Host directory for certs, mounted at /certs | {{ cs_vikunja_container_root }}/certs |
cs_vikunja_public_uri | string | Public URI for Vikunja | https://vikunja-{{ inventory_hostname }}.<cluster domain> |
cs_vikunja_extra_cors_origins | list | Extra origins to add to Vikunja's CORS origins | [] |
cs_vikunja_mailer_force_ssl | bool | Use implicit TLS against the shared SMTP endpoint | true |
cs_vikunja_cors_maxage | int | Seconds a CORS preflight response may be cached | 3600 |
cs_vikunja_trusted_proxies | list | CIDRs trusted to set X-Forwarded-For | The cluster CIDR and VPN CIDR (from Vault) |
cs_vikunja_db_cluster_name | string | Database cluster name | {{ cs_vikunja_cluster }} |
cs_vikunja_db_cluster_node | string | Database cluster node | {{ inventory_hostname }} |
cs_vikunja_db_database | string | Database name | vikunja-{{ inventory_hostname }} |
cs_vikunja_restic_cluster_name | string | Restic cluster name | {{ cs_vikunja_cluster }} |
cs_vikunja_restic_node_name | string | Restic backup node name | {{ inventory_hostname }} |
cs_vikunja_restic_repo_name | string | Restic repository name | vikunja |
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/configsecret: see PostgreSQL | Vault configurations. --tags vikunja_prepare_dbprovisions 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, pointingdatabase.sslcert/database.sslkey/database.sslrootcertat the mounted certificate directory. Vikunja has nodatabase.portoption: the port is embedded indatabase.hostashost:port. --tags vikunja_restic_restoreimports 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 fromcs_vikunja_public_uri, pluscs_vikunja_extra_cors_origins, plus anhttp://<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_originsexists for hosts wherecs_vikunja_public_uriis 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 readsVIKUNJA_CORS_ORIGINSvia viper'sGetStringSlice, which for an env-sourced string falls back to Go'scast.ToStringSliceE; for a plain string that'sstrings.Fields(v), splitting on whitespace. A comma-joined value is parsed as a single origin, so onlycs_vikunja_public_uriwould match (Vikunja re-appends it separately in code) and every other origin in the list would silently fail CORS checks.VIKUNJA_SERVICE_IPEXTRACTIONMETHOD: xffwithVIKUNJA_SERVICE_TRUSTEDPROXIESset tocs_vikunja_trusted_proxies:X-Forwarded-Foris 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 byuser change-status --enablefurther 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 apasswordfield.user set-admin: the task exists but is switched off (when: false), because the admin-panel license feature is not active, so theis_adminfield 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, deletescs_vikunja_container_root, and deletes thecs_vikunja_useruser, its home directory, and thecs_vikunja_groupgroup. Never included by the plainvikunjatag.
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