diff --git a/README.md b/README.md new file mode 100644 index 0000000..2803f33 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# 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..build` | Builds the package. | +| `checks..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:/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.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.