HortusFox writes export/auto-backup zips to public/backup using a fully predictable hf_backup_<timestamp>.zip name, serves that directory as real static files (ahead of its own auth-gated routes), and never deletes them itself unless an external backup_path is separately configured. That leaves full data exports downloadable by anyone who knows or guesses the timestamp, indefinitely. This can't be fixed from packaging alone, but giving public/backup's tmpfiles rule an age (new backupMaxAge option, default "1d", null to disable) lets systemd-tmpfiles-clean.timer prune old exports and bound the exposure window. Every other writableDirs entry is unaffected (age stays "-"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 lines
8.2 KiB
Markdown
117 lines
8.2 KiB
Markdown
# hortusfox-web-nixos
|
|
|
|
A Nix flake that packages [HortusFox](https://github.com/danielbrendel/hortusfox-web) — a self-hosted,
|
|
PHP/Laravel-style collaborative plant-management app — from source, and provides a NixOS module to run it
|
|
as a full service: mariadb + phpfpm + nginx or caddy + ACME + the app's own cron endpoints on systemd
|
|
timers.
|
|
|
|
There is no HortusFox source code in this repo — the app itself is fetched from upstream at build time via
|
|
`php.buildComposerProject`. This repo is purely packaging and deployment glue: a `package.nix` derivation
|
|
and a `services.hortusfox-web` NixOS module that wires the built package into a real, stateful host.
|
|
|
|
## Flake outputs
|
|
|
|
| Output | What it is |
|
|
| --- | --- |
|
|
| `packages.hortusfox-web` / `packages.default` | The built HortusFox package. |
|
|
| `overlays.default` | Adds `hortusfox-web` to `pkgs`. |
|
|
| `nixosModules.default` | The `services.hortusfox-web` module. |
|
|
| `devShells.default` | PHP (matching the module's build) + extensions, composer, `just`, `nix-output-monitor`. |
|
|
| `checks.<system>.build` | Builds the package. |
|
|
| `checks.<system>.vm-test` / `vm-test-caddy` | Boots a full nixosTest VM per proxy backend and exercises a real login. |
|
|
| `nixosConfigurations.test` | A throwaway VM (`tests/`) for manual poking — not meant for real deployments. |
|
|
|
|
## Usage
|
|
|
|
```nix
|
|
{
|
|
inputs.hortusfox-web.url = "github:<you>/hortusfox-web-nixos";
|
|
|
|
outputs = { self, nixpkgs, hortusfox-web, ... }: {
|
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
|
system = "x86_64-linux";
|
|
modules = [
|
|
hortusfox-web.nixosModules.default
|
|
{
|
|
services.hortusfox-web = {
|
|
enable = true;
|
|
domain = "plants.example.com";
|
|
adminAccount.passwordFile = "/run/secrets/hortusfox-admin-password";
|
|
adminAccount.email = "you@example.com";
|
|
secretFile = "/run/secrets/hortusfox.env"; # APP_KEY, SMTP_PASSWORD, ...
|
|
};
|
|
|
|
# proxy.ssl defaults to true (forceSSL + ACME); the host config
|
|
# must then set one of these globally:
|
|
security.acme = { acceptTerms = true; defaults.email = "you@example.com"; };
|
|
# or, if services.hortusfox-web.proxy.server = "caddy":
|
|
# services.caddy.email = "you@example.com";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
## `services.hortusfox-web` options
|
|
|
|
| Option | Type | Default | Description |
|
|
| --- | --- | --- | --- |
|
|
| `enable` | bool | `false` | Enable the service. |
|
|
| `domain` | string | `"localhost"` | FQDN served over HTTPS; used for the nginx/caddy vhost and ACME. |
|
|
| `port` | null or port | `null` | Appended to the app's own canonical URL (`APP_URL`) and, when set, overrides the `SERVER_PORT` fastcgi param/env seen by PHP. Needed whenever the proxy is only reachable through a forwarded/non-standard port (e.g. the test VM's QEMU hostfwd) — Asatru's own `url()`/`asset()` helpers build every absolute URL from the *request's* `SERVER_PORT`, not from `APP_URL`, so without this every generated asset/redirect URL silently drops the real external port. Does not change what port nginx/caddy actually listen on. |
|
|
| `package` | package | built from `package.nix` | The HortusFox package to deploy. |
|
|
| `proxy.server` | `"nginx"` \| `"caddy"` | `"nginx"` | Reverse proxy/webserver fronting phpfpm. |
|
|
| `proxy.ssl` | bool | `true` | Force HTTPS + request an ACME cert for the vhost. Set `false` for local/test deployments with no real domain or internet access to reach Let's Encrypt. |
|
|
| `phpPackage` | package | `pkgs.php83` | Base PHP the phpfpm environment is built from (extensions are added automatically). |
|
|
| `user` / `group` | string | `"hortusfox"` | System user/group the app and its phpfpm pool run as. |
|
|
| `timezone` | string | `"UTC"` | App timezone (`APP_TIMEZONE`). |
|
|
| `database.name` | string | `"hortusfox"` | MariaDB database name. |
|
|
| `secretFile` | null or path | `null` | File of `KEY=VALUE` lines (`APP_KEY`, `SMTP_PASSWORD`, API keys, ...) appended verbatim onto the generated `.env` at runtime — not parsed, so it can carry arbitrary keys. Keep it out of the Nix store in production (agenix/sops-nix); a store path is fine for throwaway test configs. |
|
|
| `backupMaxAge` | null or string | `"1d"` | How long HortusFox's own export/auto-backup zips (`public/backup`, systemd-tmpfiles "age" syntax, e.g. `"1d"`, `"12h"`) are kept before auto-deletion. HortusFox names these with a fully predictable `hf_backup_<timestamp>.zip` pattern and serves them from the public webroot without ever cleaning them up itself, so anyone who knows or guesses the timestamp can otherwise download a full data export indefinitely. `null` disables cleanup entirely (not recommended for internet-facing deployments). |
|
|
| `adminAccount.passwordFile` | null or path | `null` | Plaintext password file for a seeded initial admin account. HortusFox has no CLI/seed hook of its own and its only "create user" flow lives behind the admin panel, so without this a fresh deployment has no way to log in at all. Only takes effect on first boot; changing it later has no effect on an existing account. |
|
|
| `adminAccount.username` | string | `"admin"` | Name for the seeded admin account — purely a display name, since HortusFox logs in by email rather than this field. |
|
|
| `adminAccount.email` | string | `"admin@${domain}"` | Email for the seeded admin account. The default is just a guess at a real mailbox — set it to an address you actually control if you intend to use HortusFox's "Restore password" flow, which emails it. |
|
|
| `settings` | attrs of str/int/bool | `{ }` | Extra/overriding `.env` entries, merged over the module's generated defaults (`settings` always wins). Note `php asatru` (used for migrations) only runs when `APP_DEBUG` is true, so migrations silently no-op unless it's set here or via `secretFile`. |
|
|
| `smtp.host` | string | `""` | SMTP server host. |
|
|
| `smtp.port` | port | `587` | SMTP server port. |
|
|
| `smtp.username` | string | `""` | SMTP username. |
|
|
| `smtp.fromName` | string | `"HortusFox"` | SMTP from-name. |
|
|
| `smtp.fromAddress` | string | `"hortusfox@localhost"` | SMTP from-address. |
|
|
| `smtp.encryption` | `"tls"` \| `"ssl"` \| `""` | `"tls"` | SMTP transport encryption. |
|
|
|
|
SMTP password isn't a module option — set `SMTP_PASSWORD` via `secretFile` instead, alongside `APP_KEY`.
|
|
|
|
## What the module actually does
|
|
|
|
- Copies the read-only Nix store package into a writable runtime tree (`/run/hortusfox/www`) on every boot,
|
|
then symlinks a handful of subdirectories (`cache`, `app/logs`, `public/img`, `public/attachments`,
|
|
`public/backup`, `public/themes`) into persistent state under `/var/lib/hortusfox`, seeding them from the
|
|
package's own shipped defaults first so nothing gets silently deleted on first boot.
|
|
- Renders `.env` from the module's curated + mirrored-upstream defaults, `settings` (always wins), and
|
|
`secretFile` (appended verbatim).
|
|
- Runs `php asatru migrate:fresh` on first boot (schema only) and `migrate:upgrade` on every boot after,
|
|
guarded by a `stateDir/.installed` marker so a destructive fresh migration never runs twice.
|
|
- Optionally seeds a single admin account directly into the database on first boot, since HortusFox has no
|
|
other way to create the very first user.
|
|
- Drives HortusFox's own HTTP cron endpoints (task/calendar reminders, auto-backup) via systemd timers
|
|
rather than an in-process scheduler.
|
|
|
|
See `nixos-module/default.nix` and `CLAUDE.md` for the full reasoning and the gotchas each of these design
|
|
choices worked around (several were real, previously-shipped bugs).
|
|
|
|
## Local development / testing
|
|
|
|
```bash
|
|
nix develop # dev shell: PHP+extensions, composer, just, nix-output-monitor
|
|
just build-vm # build the throwaway test VM (nixosConfigurations.test)
|
|
just boot-vm # boot it in-terminal (QEMU, serial console); http://127.0.0.1:8080
|
|
just clean # tear down local VM artifacts (this project's own qemu instance + build outputs)
|
|
|
|
nix flake check # evaluate all outputs; builds the package + boots both proxy-backend VM tests
|
|
nix flake check -L # same, with full build/VM logs streamed (slow: boots two real VMs)
|
|
```
|
|
|
|
The test VM's seeded admin account logs in with the password in `tests/hortusfox_web_test_password`
|
|
(currently `password`) — see `tests/default.nix` for its full config.
|