- Shell 41.5%
- Makefile 27%
- HCL 26.8%
- Jinja 4.7%
|
|
||
|---|---|---|
| .agents/skills | ||
| docs | ||
| npm | ||
| vikunja | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| GLOSSARY.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| skills-lock.json | ||
infra-tracker
OpenTofu + Ansible that provision and maintain a self-hosted task tracker on the Pit homelab.
The repo name and the stack are deliberately generic so the tool can be swapped
later; the folder is named vikunja/ because that is what runs today.
Layout
Organised by feature, not by tool: each feature folder holds its own layers.
One root .env carries the secrets for the whole repo.
Makefile root: the guest, service and edge targets
.env root: SECRETS — gitignored, never committed
vikunja/guest/ OpenTofu: the LXC guest stack
vikunja/service/ Ansible: the native install on that guest
npm/ OpenTofu: the Edge stack (the NPM proxy host)
The guest stack (vikunja/guest/)
One unprivileged Debian 13 LXC on the Proxmox cluster. Ansible then installs the task tracker on it natively.
| Thing | Value |
|---|---|
| Node | burns |
| vmid | 142 |
| hostname | vikunja |
| IP | 10.12.0.142/24, gateway 10.12.0.1 |
| Bridge | vmbr1 |
| Rootfs datastore | fast-storage |
| Cores / memory / swap / disk | 2 / 2048 / 512 / 16 GB |
| unprivileged / nesting | true / true |
| Provider | bpg/proxmox, ~> 0.77 |
Nesting is true and is not optional. Modern systemd requires it; Debian 13
with nesting disabled comes up degraded (services failing 243/CREDENTIALS,
empty console).
The base image: declared, then imported once
The Debian 13 template on the synology datastore is managed by infra-forge.
This stack declares the same proxmox_download_file resource (same URL and
checksum), but the file already exists on the node, so import it once before the
first apply:
tofu import proxmox_download_file.debian_13_base \
burns/synology:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst
The <node>/<datastore>:<content_type>/<file> shape is required; the import
fails without the node prefix.
The service stack (vikunja/service/)
Ansible installs Vikunja natively on the guest: no container, no orchestration.
| Thing | Value |
|---|---|
| Version | Vikunja v2.7.0, downloaded checksum-first |
| Binary | /opt/vikunja/vikunja (one binary serves the API and the frontend) |
| Config | /etc/vikunja/config.yml, 0640 root:vikunja, passed via --config |
| Home | /var/lib/vikunja (files/basepath under it) |
| Database | PostgreSQL 17 on the same guest (package postgresql-17, asserted), db and role vikunja |
| Unit | vikunja.service, sandboxed (ProtectSystem=strict, NoNewPrivileges) |
| Public URL | https://vikunja.thepit.space/ |
| Logs | the journal (log.standard defaults to stdout) |
| Client IP | ipextractionmethod: xff, trusting the Edge (10.12.0.113) via trustedproxies |
The role is idempotent: a second run changes nothing. user create is not, so
the role guards it with user list --email and only creates what is missing.
Secrets
The prod Vault holds the values, encrypted, in
vikunja/service/inventories/prod/group_vars/vikunja/vault.yml (committed as
ciphertext). It sits beside the prod Inventory so a test run selects the
dev/ci Vault instead (ADR-0002):
| Value | Key |
|---|---|
| PostgreSQL password | vault_vikunja_db_password |
| Service JWT secret | vault_vikunja_service_secret |
| Account passwords | vault_vikunja_account_passwords (by username) |
| SMTP password | vault_vikunja_smtp_password |
The prod Vault's password lives in the root .env (ANSIBLE_VAULT_PASSWORD);
the Makefile derives the gitignored vikunja/service/vault-pass from it, so the
operator keeps one secrets file. The dev/ci Vault has its own password, held
only in the CI secret store.
The mailer is on: vault_vikunja_smtp_password is set, so mailer.enabled
resolves true. Empty that value to turn mail off again.
Account passwords are create-time only. Changing one in the Vault and re-running does not change the live password, because the role only creates missing accounts (and the CLI has no non-interactive password change).
Adding an account is a two-file edit — the entry in
roles/vikunja/defaults/main.yml (vikunja_accounts) and its password under
vault_vikunja_account_passwords in the Vault. The role asserts every listed
account has a password, so a missing one fails with a clear message instead of
crashing mid-loop.
cd vikunja/service
ansible-vault edit inventories/prod/group_vars/vikunja/vault.yml # needs vault-pass present
The Edge stack (npm/)
One Nginx Proxy Manager proxy host publishes the Tracker from the WAN. It follows
infra-forge's Edge: only the host named in edge.tfvars is managed, TLS is looked
up rather than created, and everything else in NPM is left alone.
| Thing | Value |
|---|---|
| Published site | https://vikunja.thepit.space/ |
| Target | 10.12.0.142:3456 (the Tracker Guest), scheme http |
| TLS | NPM certificate looked up by domain; highest id wins |
| Host flags | SSL forced, HTTP/2, block-exploits, websockets; no HSTS, no caching |
| Provider | Sander0542/nginxproxymanager, ~> 1.4 |
The certificate is a prerequisite, and a UI step
The stack looks up the Let's Encrypt certificate for vikunja.thepit.space; it
never creates one (this NPM build rejects the provider's certificate_letsencrypt
create). Request the cert once in the NPM UI (Let's Encrypt, HTTP-01); NPM
auto-renews it thereafter. If it is missing, make edge-plan fails on purpose with
a precondition naming the domain, rather than publishing the host without TLS.
Running it
make edge-plan # show the plan; mutates nothing
make edge # plan, render, confirm, apply
make verify # external check: https 200, http 301
Using it
cp .env.example .env # fill in the Proxmox endpoint, API token, vault password, NPM creds
make guest-plan # show the guest plan; mutates nothing
make guest # plan, render, confirm, apply
make service-check # rehearse the install with --check --diff
make service # check, then install for real
make edge-plan # show the edge plan; mutates nothing
make edge # plan, render, confirm, apply
make verify # external check: https 200, http 301
make fmt # tofu fmt -recursive
make validate # validate every stack (guest and edge)
Secrets live in the root .env (PROXMOX_VE_ENDPOINT,
PROXMOX_VE_API_TOKEN, ANSIBLE_VAULT_PASSWORD, NGINXPROXYMANAGER_URL,
NGINXPROXYMANAGER_USERNAME, NGINXPROXYMANAGER_PASSWORD). It is gitignored and
never committed.
Conventions
- Work on a branch, open a PR. Nothing lands directly on
main. - Never commit our addresses: the DDNS hostname and the external IP stay out of the repo. Public service domains and upstream mirrors are fine.