Files
hortusfox-web-nixos/README.md
T
awkawbandClaude Sonnet 5 d88f6f312d Add adminAccount.username, decoupled from the seeded admin's hardcoded name
The seeded admin account's UserModel "name" was hardcoded to "admin".
Make it a separate, overridable option (still defaulting to "admin"
for compatibility) -- it's purely a display name, since HortusFox logs
in by email rather than this field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:52:33 -04:00

116 lines
7.6 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. |
| `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.