Phase 1 of the incus-pet design. Adds the deployment infrastructure under modules/nixos/linux/incus/ — a CLI, a container-essentials NixOS module, a SKILL.md for agent-driven adoption — and deletes the webapps/ tree (nspawn-based, weaker isolation defaults, no live consumers). Pairs with srid/anywhen#15 (nixosModules.incus on the anywhen flake) for end-to-end deployment of anywhen as an incus container. Surface added: - modules/nixos/linux/incus/container.nix container essentials (sshd, hostname, flakes, firewall, base packages); imported by the marshaling flake incus-pet generates per app - modules/nixos/linux/incus/incus-pet/ CLI tree: default.nix writeShellApplication; subcommands deploy/list/rm SKILL.md 3-branch agent recipe (deploy / wrapper / add contract upstream) README.md human docs with the full network-model section - modules/flake-parts/incus-pet.nix exposes packages.incus-pet Surface removed: - modules/nixos/linux/anywhen.nix anywhen runs as a container now, not as a host-installed service - configurations/nixos/pureintent drops the anywhen import and the services.anywhen.host wiring - webapps/ deleted entirely Port convention: every containerized service binds 8080 inside its own netns. The host-side <listen-ip>:<host-port> is unique per app, chosen at first deploy via --port + --listen (or INCUS_PET_LISTEN env), and recorded in container metadata so re-deploys are flagless. Run: nix run .#incus-pet -- deploy github:srid/anywhen \ --port 7700 --listen 100.122.32.106 See modules/nixos/linux/incus/incus-pet/README.md for the operator flow and the network model in full.
4.7 KiB
| name | description | argument-hint |
|---|---|---|
| incus-pet | Deploy a NixOS service flake into a per-app incus container ("pet PaaS") on the local host — OR add the required nixosModules.incus contract to a flake so it becomes deployable. Use when the user says "deploy X", "make this app deployable", "add incus to this flake", or "publish this on my box". | deploy <flake-ref> [<name>] [--port N] [--listen IP] |
incus-pet
A per-app incus container deploys a flake-shipped NixOS service onto the
local host, exposed on a chosen <listen-ip>:<host-port> that proxies to
a fixed 8080 inside the container.
Pick the branch from context:
- User in their OWN app's repo, "make this deployable" → Branch C
- User wants to add the contract to an UPSTREAM they can PR to (e.g. juspay/kolu) — agent has push/PR access → Branch C
- Flake-ref already ships
nixosModules.incus→ Branch A - Upstream won't accept a PR and user explicitly wants a local wrapper → Branch B
Branch A — deploy a flake that ships the contract
Run:
incus-pet deploy <flake-ref> [<name>] [--port N] [--listen IP]
--port is required on the first deploy of a given <name> and recorded
in container metadata; subsequent deploys read it back. Report the
<listen>:<port> URL to the user.
Branch B — local stand-alone wrapper (RARE; Branch C is preferred)
Only when upstream won't accept the contract. Create a tiny wrapper flake locally:
flake.nixexposingnixosModules.incusper the contractservice-module.nixauthoringservices.<app>as a NixOS system module (mirror the upstream's home-manager module if any)- Add the upstream as a flake input
Then incus-pet deploy ./path/to/wrapper <app>.
Branch C — add the contract to a flake (yours or upstream-via-PR)
Steps:
-
Verify the flake exposes
packages.<system>.default. If not, stop — prerequisite is a package derivation withmeta.mainProgram. -
Verify
nixosModules.default(the service module). If MISSING but the flake hashomeManagerModules.default(kolu case):- Author
nix/nixos/module.nixas a system NixOS module mirroring the home-manager surface — same options (enable,package,host,port, ...), emittingsystemd.services.<app>instead of a home-manager unit. - Expose as
nixosModules.defaultin flake outputs.
If MISSING entirely (no module at all), stop — prerequisite.
- Author
-
Add
nixosModules.incusto outputs (fill in<app>):nixosModules.incus = { config, lib, pkgs, ... }: { imports = [ self.nixosModules.default ]; incus.container = { enable = true; hostname = lib.mkDefault "<app>"; }; system.stateVersion = "25.05"; services.<app> = { enable = true; package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default; host = lib.mkDefault "0.0.0.0"; port = 8080; # fixed by the incus-pet contract }; }; -
If
services.<app>doesn't expose{host, port, package}(or equivalents), surface as prerequisite. Do not invent options that aren't there — add them to the service module first. -
Smoke-test:
nix flake check nix eval .#nixosModules.incus -
If upstream (PR mode), open the PR. Else commit.
-
Tell the user how to deploy:
incus-pet deploy github:<owner>/<repo> --port <chosen> --listen <ip>
Do NOT add example/-style sub-flakes or VM tests in this branch —
separate scope.
Contract for nixosModules.incus
The module must set:
incus.container.enable = trueincus.container.hostname = "<app>"(mkDefaultok)system.stateVersion = "25.05"services.<app>.{enable, package, host, port}(port = 8080, hardcoded — this is the incus-pet convention)
The module must NOT set:
services.<app>.portto anything other than8080networking.firewall.allowedTCPPorts—container.nixopens 8080
Idempotence + failure modes
- Re-running
incus-pet deploy <flake-ref>against an existing container is safe: marshaling flake is rewritten and re-locked; container bootstrap is idempotent; proxy device issetif present, elseadd. - Container metadata (
user.incus-pet.{host-port,listen,flake-ref}) is the source of truth for the operator-chosen exposure.incus-pet listfilters by theflake-refkey. - If
--portis missing on the first deploy, the command fails before launching anything. - Network failures during
nixos-rebuild --target-hostleave the container running its previous generation —nixos-rebuild switchis itself atomic.