deploy/README.md
Rama main · réplica de lectura del repositorio original.
# Deploying Stocktake
Several ways in, in order of how much you already run.
| | For | Database |
| --- | --- | --- |
| [`compose/`](compose/) | One machine, a NAS, a home server | SQLite |
| [`nas/`](nas/) | Unraid · TrueNAS · HexOS · Synology · QNAP · CasaOS · Proxmox | SQLite |
| [`kubernetes/`](kubernetes/) | A cluster, without Helm | SQLite |
| [`helm/stocktake/`](helm/stocktake/) | A cluster, with Helm | SQLite (default) |
| [`helm/stocktake/`](helm/stocktake/) + values | A cluster, sharing your Postgres | Postgres |
The Helm chart is published with each release as
`oci://ghcr.io/jarrodruggiero/charts/stocktake`, at the app's version;
`helm/stocktake/` is its source, and names no release until one is packaged.
**All of them run the same image.** No platform gets a variant, and nothing
here needs building per-NAS — the differences are entirely in how each one
takes a container definition. Most take the Compose file unchanged.
Nothing needs a config file written beforehand either: with no configuration
the app boots into a first-run wizard that chooses the database, creates your
account, and writes `config.yaml` itself.
No Redis, no queue, no secrets service — sessions are database-backed tokens,
and the price feed needs no credentials.
## The two directories, and why only one is backed up
**`/data` is everything you own** — the database, and any broker or statement
formats you added. Back it up with `config.yaml`: together they are the whole
application state ([how](https://jarrodruggiero.github.io/stocktake/deploy/#backing-up-and-restoring)).
**`/scratch` is deliberately disposable.** The visual statement mapper renders
page images there while somebody is using it, and sweeps them thirty minutes
after they stop. A broker CSV's rows wait there between its preview and its
commit, and are swept after a day. The Kubernetes manifest and the Helm chart
mount an `emptyDir` over it, with a size limit; under Docker it is just a
directory in the container. **Do not put it on a persistent volume and do not
back it up** — these are pictures of a dividend statement and the trades you
were about to import, they are written `0700` so one signed-in user cannot
read another's, and a snapshot repository keeping seven daily copies is the
opposite of what the app promises about them.
There is a practical reason too. Back /scratch up and the backup itself breaks:
a restic mover that drops `DAC_OVERRIDE` cannot traverse a `0700` directory it
does not own, so it exits non-zero and retries forever.
## Choosing a database
**SQLite, unless you know you need otherwise.** One file, nothing else to run,
backed up by copying it. A personal portfolio is a few thousand rows.
**Postgres** if you already run one and would rather keep everything in it.
Either way the app runs as **one replica**: the import preview, the statement
mapper and the price feed each live in its one process, so a second replica
would lose an import whose preview and commit reached different pods.
The Helm chart deliberately ships **no bundled Postgres**. A database that
appears and disappears with `helm uninstall` is a way to lose your data — point
the chart at one you run and back up.
## Three things everyone gets wrong
**The timezone.** Containers run UTC. Every date decision in the app — which
financial year today is in, whether a trade is dated in the future — uses the
configured zone, so leaving it unset puts the app a day behind for most of an
Australian morning.
**`trusted_proxies` behind a reverse proxy.** It is how the app learns whether
the connection was encrypted and which address it came from, both of which a
proxy hides. Without it, a proxy that terminates TLS makes every request look
like plain HTTP, so the login page warns about plain HTTP when you are not on it
and passkeys are not offered, and every sign-in is recorded against the proxy's
address. Lockout counts failures per account, so it does not depend on this. Set
it to your proxy's address or CIDR and nothing wider, because a caller whose
address is believed can choose the one recorded against their sign-ins.
**`cookie_secure` once TLS is in front.** Until then the session cookie travels
in clear. The Helm chart refuses to render an Ingress without it rather than
letting that happen quietly.
## Storage permissions
The image runs as **uid 1000**, and the volume has to be writable by it:
- **Kubernetes** — `fsGroup: 1000` on the pod. Both the manifests and the chart
set it. Without it the PVC arrives owned by root and the app cannot create
its database.
- **Compose** — a *named* volume inherits the image's ownership and just works.
A **bind** mount does not: `chown -R 1000:1000 ./data` on the host first.
## Upgrading
The app migrates its own database at startup. **Back up first.** Migrations
roll forward; rolling an image back after one has run is not supported.
Config and app version are a matched pair — the app rejects unknown settings
outright, so a typo is loud rather than ignored, and a config written for a
newer version is refused by an older one. Move them together.
## Backups
What to back up, how, and how to restore, on every platform:
[Backing up and restoring](https://jarrodruggiero.github.io/stocktake/deploy/#backing-up-and-restoring).