mirror of
https://github.com/srid/nixos-config.git
synced 2026-07-16 22:01:33 +08:00
incus-pet: per-app incus container CLI ("pet PaaS"); drop webapps/
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.
This commit is contained in:
parent
db61baee31
commit
cc95849e4e
13 changed files with 914 additions and 135 deletions
|
|
@ -28,10 +28,10 @@ in
|
|||
./devbox.nix
|
||||
(self + /modules/nixos/linux/beszel.nix)
|
||||
(self + /modules/nixos/linux/incus)
|
||||
(self + /modules/nixos/linux/anywhen.nix)
|
||||
];
|
||||
|
||||
services.anywhen.host = "100.122.32.106"; # Tailscale IP of pureintent
|
||||
# anywhen runs as an incus-pet container now (see modules/nixos/linux/incus/incus-pet);
|
||||
# deploy with: incus-pet deploy github:srid/anywhen --port 7700 --listen 100.122.32.106
|
||||
|
||||
# Expose the incus UI on the Tailscale interface only.
|
||||
virtualisation.incus.preseed.config."core.https_address" = "100.122.32.106:8443";
|
||||
|
|
|
|||
203
flake.lock
generated
203
flake.lock
generated
|
|
@ -44,17 +44,20 @@
|
|||
}
|
||||
},
|
||||
"anywhen": {
|
||||
"inputs": {
|
||||
"bun2nix": "bun2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779551219,
|
||||
"narHash": "sha256-Mz6QfMmBmZeCBocvvWq8juwp7j4r78nRvVwkmxTFQM4=",
|
||||
"lastModified": 1779558463,
|
||||
"narHash": "sha256-diiqb7KW8oe4X2Q8ki2fj1QUeIqBuVZpkBOq6xwwyIo=",
|
||||
"owner": "srid",
|
||||
"repo": "anywhen",
|
||||
"rev": "a6d5ed84bf70049c7f3349e6ed218952ac5d8a2d",
|
||||
"rev": "01e72a05fcaab3d1fb0ff99808c3339abacc47ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "srid",
|
||||
"ref": "abject-turn",
|
||||
"ref": "incus-contract",
|
||||
"repo": "anywhen",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
@ -85,12 +88,35 @@
|
|||
}
|
||||
},
|
||||
"bun2nix": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"import-tree": "import-tree",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779553062,
|
||||
"narHash": "sha256-T5cVey80VVqPIkZXHX8kaIxhrqGKw97Nma6ufTfy/dY=",
|
||||
"owner": "juspay",
|
||||
"repo": "bun2nix",
|
||||
"rev": "948386f5219e4526a2978110dacffb433cef1013",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "juspay",
|
||||
"ref": "rawflake",
|
||||
"repo": "bun2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"bun2nix_2": {
|
||||
"inputs": {
|
||||
"flake-parts": [
|
||||
"llm-agents",
|
||||
"flake-parts"
|
||||
],
|
||||
"import-tree": "import-tree",
|
||||
"import-tree": "import-tree_2",
|
||||
"nixpkgs": [
|
||||
"llm-agents",
|
||||
"nixpkgs"
|
||||
|
|
@ -233,11 +259,11 @@
|
|||
},
|
||||
"disc-scrape": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-flake": "rust-flake",
|
||||
"systems": "systems_2"
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772235986,
|
||||
|
|
@ -296,14 +322,14 @@
|
|||
"commonmark-wikilink": "commonmark-wikilink",
|
||||
"ema": "ema",
|
||||
"emanote-template": "emanote-template",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"flake-parts": "flake-parts_3",
|
||||
"fourmolu-nix": "fourmolu-nix",
|
||||
"git-hooks": "git-hooks_2",
|
||||
"haskell-flake": "haskell-flake",
|
||||
"heist-extra": "heist-extra",
|
||||
"lvar": "lvar",
|
||||
"nixos-unified": "nixos-unified",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"unionmount": "unionmount"
|
||||
},
|
||||
"locked": {
|
||||
|
|
@ -337,6 +363,24 @@
|
|||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"disc-scrape",
|
||||
|
|
@ -357,7 +401,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"flake-parts_3": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"emanote",
|
||||
|
|
@ -378,9 +422,9 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_3": {
|
||||
"flake-parts_4": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
"nixpkgs-lib": "nixpkgs-lib_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772408722,
|
||||
|
|
@ -396,7 +440,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_4": {
|
||||
"flake-parts_5": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"imako",
|
||||
|
|
@ -417,7 +461,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_5": {
|
||||
"flake-parts_6": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"llm-agents",
|
||||
|
|
@ -438,7 +482,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_6": {
|
||||
"flake-parts_7": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"vira",
|
||||
|
|
@ -461,7 +505,7 @@
|
|||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_4"
|
||||
"systems": "systems_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
|
|
@ -753,12 +797,12 @@
|
|||
"aeson-typescript": "aeson-typescript",
|
||||
"commonmark-simple": "commonmark-simple_2",
|
||||
"commonmark-wikilink": "commonmark-wikilink_2",
|
||||
"flake-parts": "flake-parts_4",
|
||||
"flake-parts": "flake-parts_5",
|
||||
"fourmolu-nix": "fourmolu-nix_2",
|
||||
"git-hooks": "git-hooks_4",
|
||||
"haskell-flake": "haskell-flake_2",
|
||||
"lvar": "lvar_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"process-compose-flake": "process-compose-flake",
|
||||
"unionmount": "unionmount_2"
|
||||
},
|
||||
|
|
@ -791,6 +835,21 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"import-tree_2": {
|
||||
"locked": {
|
||||
"lastModified": 1763762820,
|
||||
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"jumphost-nix": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
|
@ -842,13 +901,13 @@
|
|||
"llm-agents": {
|
||||
"inputs": {
|
||||
"blueprint": "blueprint",
|
||||
"bun2nix": "bun2nix",
|
||||
"flake-parts": "flake-parts_5",
|
||||
"bun2nix": "bun2nix_2",
|
||||
"flake-parts": "flake-parts_6",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_3",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
"systems": "systems_4",
|
||||
"treefmt-nix": "treefmt-nix_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775790657,
|
||||
|
|
@ -1048,21 +1107,36 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772082373,
|
||||
"narHash": "sha256-wySf8a6hvuqgFdwvvzPPTARBCMLDz7WFAufGkllD1M4=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "26eaeac4e409d7b5a6bf6f90a2a2dc223c78d915",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1769909678,
|
||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772328832,
|
||||
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
|
||||
|
|
@ -1078,6 +1152,22 @@
|
|||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772082373,
|
||||
"narHash": "sha256-wySf8a6hvuqgFdwvvzPPTARBCMLDz7WFAufGkllD1M4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "26eaeac4e409d7b5a6bf6f90a2a2dc223c78d915",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1752900028,
|
||||
"narHash": "sha256-dPALCtmik9Wr14MGqVXm+OQcv7vhPBXcWNIOThGnB/Q=",
|
||||
|
|
@ -1093,7 +1183,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1770169770,
|
||||
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
|
||||
|
|
@ -1109,7 +1199,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1778443072,
|
||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
||||
|
|
@ -1125,7 +1215,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"nixpkgs_6": {
|
||||
"locked": {
|
||||
"lastModified": 1765803225,
|
||||
"narHash": "sha256-xwaZV/UgJ04+ixbZZfoDE8IsOWjtvQZICh9aamzPnrg=",
|
||||
|
|
@ -1141,7 +1231,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_6": {
|
||||
"nixpkgs_7": {
|
||||
"locked": {
|
||||
"lastModified": 1764635402,
|
||||
"narHash": "sha256-6rYcajRLe2C5ZYnV1HYskJl+QAkhvseWTzbdQiTN9OI=",
|
||||
|
|
@ -1229,7 +1319,7 @@
|
|||
"disc-scrape": "disc-scrape",
|
||||
"disko": "disko",
|
||||
"emanote": "emanote",
|
||||
"flake-parts": "flake-parts_3",
|
||||
"flake-parts": "flake-parts_4",
|
||||
"git-hooks": "git-hooks_3",
|
||||
"github-nix-ci": "github-nix-ci",
|
||||
"home-manager": "home-manager",
|
||||
|
|
@ -1243,7 +1333,7 @@
|
|||
"nixos-hardware": "nixos-hardware",
|
||||
"nixos-unified": "nixos-unified_2",
|
||||
"nixos-vscode-server": "nixos-vscode-server",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"nixpkgs": "nixpkgs_5",
|
||||
"project-unknown": "project-unknown",
|
||||
"vira": "vira",
|
||||
"zmx": "zmx"
|
||||
|
|
@ -1370,6 +1460,21 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_5": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"tabler-icons-hs": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
|
@ -1387,6 +1492,28 @@
|
|||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"anywhen",
|
||||
"bun2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770228511,
|
||||
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"llm-agents",
|
||||
|
|
@ -1444,7 +1571,7 @@
|
|||
"inputs": {
|
||||
"co-log-effectful": "co-log-effectful",
|
||||
"devour-flake": "devour-flake",
|
||||
"flake-parts": "flake-parts_6",
|
||||
"flake-parts": "flake-parts_7",
|
||||
"fourmolu-nix": "fourmolu-nix_3",
|
||||
"git-hooks": "git-hooks_5",
|
||||
"haskell-flake": "haskell-flake_3",
|
||||
|
|
@ -1454,7 +1581,7 @@
|
|||
"nix-serve-ng": "nix-serve-ng",
|
||||
"nix-systems": "nix-systems",
|
||||
"nixos-unified": "nixos-unified_3",
|
||||
"nixpkgs": "nixpkgs_5",
|
||||
"nixpkgs": "nixpkgs_6",
|
||||
"process-compose-flake": "process-compose-flake_2",
|
||||
"record-hasfield": "record-hasfield",
|
||||
"servant-event-stream": "servant-event-stream",
|
||||
|
|
@ -1494,7 +1621,7 @@
|
|||
"zig2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_6"
|
||||
"nixpkgs": "nixpkgs_7"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764678235,
|
||||
|
|
|
|||
|
|
@ -38,8 +38,11 @@
|
|||
# KOLU
|
||||
kolu.url = "github:juspay/kolu/feat/dock-activity-window";
|
||||
|
||||
# anywhen — personal task manager (PR #2 adds NixOS module)
|
||||
anywhen.url = "github:srid/anywhen/abject-turn";
|
||||
# anywhen — personal task manager. The `incus-contract` branch
|
||||
# (srid/anywhen#15) stacks on `abject-turn` (#2) and adds
|
||||
# nixosModules.incus, which the incus-pet CLI
|
||||
# (modules/nixos/linux/incus/incus-pet) consumes.
|
||||
anywhen.url = "github:srid/anywhen/incus-contract";
|
||||
|
||||
project-unknown.url = "github:juspay/project-unknown";
|
||||
project-unknown.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
|
|
|||
14
modules/flake-parts/incus-pet.nix
Normal file
14
modules/flake-parts/incus-pet.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Expose the incus-pet CLI as a flake package.
|
||||
#
|
||||
# The CLI itself lives under modules/nixos/linux/incus/incus-pet/ so the
|
||||
# whole incus/ tree stays a self-contained transplant unit (when it
|
||||
# eventually lifts to its own repo, this flake-parts wiring stays
|
||||
# behind).
|
||||
{ ... }:
|
||||
{
|
||||
perSystem = { pkgs, ... }: {
|
||||
packages.incus-pet = pkgs.callPackage
|
||||
../nixos/linux/incus/incus-pet
|
||||
{ };
|
||||
};
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{ flake, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (flake) inputs;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
inputs.anywhen.nixosModules.default
|
||||
];
|
||||
|
||||
services.anywhen = {
|
||||
enable = true;
|
||||
package = inputs.anywhen.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
port = 6111;
|
||||
};
|
||||
}
|
||||
62
modules/nixos/linux/incus/container.nix
Normal file
62
modules/nixos/linux/incus/container.nix
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Container-side essentials for incus-pet — the OTHER half of this tree.
|
||||
#
|
||||
# default.nix configures the incus daemon ON THE HOST (bridge, UI, etc).
|
||||
# THIS module is imported INSIDE each containerized app's nixosConfiguration,
|
||||
# alongside the app's own nixosModules.incus. It declares the contract
|
||||
# options that every app's incus module sets, and the boring NixOS-inside-
|
||||
# a-container plumbing (sshd, base packages, firewall).
|
||||
#
|
||||
# The incus-pet CLI's marshaling flake imports this file directly by store
|
||||
# path — no flake-input dance — so this file stays as a plain NixOS module.
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.incus.container;
|
||||
in
|
||||
{
|
||||
# The CONVENTION: every containerized service binds 8080 inside its
|
||||
# own netns. The host-side incus proxy device translates a unique
|
||||
# <listen-ip>:<host-port> to <container>:8080 at deploy time. App
|
||||
# modules just hardcode `services.<app>.port = 8080;` — no shared
|
||||
# option, no forward reference, no coupling between an app's flake
|
||||
# and this tree at evaluation time.
|
||||
options.incus.container = {
|
||||
enable = lib.mkEnableOption "incus-pet container essentials";
|
||||
|
||||
hostname = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
Container hostname. Each app's nixosModules.incus sets this
|
||||
with mkDefault; the operator can override per deploy if they
|
||||
want a different name than the app's default.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
networking.hostName = cfg.hostname;
|
||||
|
||||
# NixOS-inside-a-container essentials. The community NixOS incus
|
||||
# image ships with neither sshd nor flakes; the incus-pet CLI does
|
||||
# a one-time bootstrap via `incus exec` to push the operator pubkey
|
||||
# and apply a temporary config that enables both. From the FIRST
|
||||
# `nixos-rebuild --target-host` onwards, the deployed config (this
|
||||
# module) carries those settings forward.
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PermitRootLogin = "yes";
|
||||
};
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
|
||||
# The hardcoded service port — opening it here too is cheap
|
||||
# insurance against a mis-authored app module forgetting to.
|
||||
networking.firewall.allowedTCPPorts = [ 8080 ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
git
|
||||
vim
|
||||
curl
|
||||
htop
|
||||
];
|
||||
};
|
||||
}
|
||||
184
modules/nixos/linux/incus/incus-pet/README.md
Normal file
184
modules/nixos/linux/incus/incus-pet/README.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# incus-pet
|
||||
|
||||
Per-app incus container CLI ("pet PaaS"). Takes a NixOS service flake,
|
||||
materialises it into a dedicated incus container on the local host, and
|
||||
exposes the service on a chosen `<listen-ip>:<host-port>` that proxies
|
||||
to a fixed `8080` inside the container.
|
||||
|
||||
```
|
||||
incus-pet deploy github:srid/anywhen --port 7700 --listen 100.122.32.106
|
||||
incus-pet list
|
||||
incus-pet rm anywhen
|
||||
```
|
||||
|
||||
See `SKILL.md` for the agent-facing recipe; this README is for humans
|
||||
running the CLI directly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The host runs the incus daemon (this repo's `modules/nixos/linux/incus`
|
||||
enables it).
|
||||
- The operator's user is in the `incus-admin` group.
|
||||
- An SSH key exists at `~/.ssh/id_ed25519.pub` (or `~/.ssh/id_rsa.pub`)
|
||||
that root inside the container will trust.
|
||||
- The target flake ships `nixosModules.incus` (see `SKILL.md` Branch C
|
||||
for what that means, or the anywhen flake for a worked example).
|
||||
|
||||
## The deploy unit — what a flake must ship
|
||||
|
||||
```nix
|
||||
# in the app's flake.nix outputs
|
||||
nixosModules.incus = { config, lib, pkgs, ... }: {
|
||||
imports = [ self.nixosModules.default ];
|
||||
incus.container = {
|
||||
enable = true;
|
||||
hostname = lib.mkDefault "anywhen";
|
||||
};
|
||||
system.stateVersion = "25.05";
|
||||
services.anywhen = {
|
||||
enable = true;
|
||||
package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
host = lib.mkDefault "0.0.0.0";
|
||||
port = 8080; # the incus-pet contract — fixed
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
The CLI's marshaling flake imports this alongside `container.nix` (the
|
||||
in-tree essentials) and runs `nixos-rebuild switch --target-host`
|
||||
against the container.
|
||||
|
||||
## Network model — fixed inside, unique outside
|
||||
|
||||
This is the section worth reading twice.
|
||||
|
||||
### Inside the container
|
||||
|
||||
Every containerized service binds **the same port**: `8080`. This is a
|
||||
convention, declared as a `readOnly` option (`incus.container.servicePort`)
|
||||
in `container.nix`. App authors do not pick a port — they wire
|
||||
`services.<app>.port = config.incus.container.servicePort` and move on.
|
||||
Cross-app port collisions become impossible because there is only one
|
||||
port to collide with, and each container has its own network namespace.
|
||||
|
||||
### Outside the container (the host)
|
||||
|
||||
The host runs an **incus proxy device** per container that translates a
|
||||
unique `<listen-ip>:<host-port>` on the host into a connection to the
|
||||
container's `127.0.0.1:8080`:
|
||||
|
||||
```
|
||||
incus config device add <name> web proxy \
|
||||
listen=tcp:<listen-ip>:<host-port> \
|
||||
connect=tcp:127.0.0.1:8080
|
||||
```
|
||||
|
||||
`incus-pet deploy` wires this for you idempotently. The `<host-port>`
|
||||
and `<listen-ip>` are stored in container metadata
|
||||
(`user.incus-pet.host-port`, `user.incus-pet.listen`) so re-running
|
||||
`incus-pet deploy <flake-ref>` doesn't need the flags repeated.
|
||||
|
||||
### Picking a listen IP
|
||||
|
||||
`<listen-ip>` can be any address bound on the host. The three meaningful
|
||||
choices:
|
||||
|
||||
| Listen IP | Reach | Firewall edit needed? |
|
||||
|------------------|----------------------|---------------------------------------------|
|
||||
| `0.0.0.0` | Anyone who can route to this host's NIC | Yes — open `<host-port>` in `networking.firewall.allowedTCPPorts` |
|
||||
| Host's LAN IP | Same as above, scoped to that NIC | Yes — same as above |
|
||||
| Host's tailnet IP (e.g. `100.122.32.106`) | Tailnet only | **No** — `tailscale0` is in `networking.firewall.trustedInterfaces` (see `configurations/nixos/pureintent/default.nix:78`), so traffic on that interface bypasses the host firewall |
|
||||
|
||||
For "internal apps on my box, reachable only from my tailnet" (the
|
||||
common case), pick the tailscale IP. On a host where you always want
|
||||
that, export the IP once:
|
||||
|
||||
```sh
|
||||
# in your shell profile, or via environment.variables in the host config
|
||||
export INCUS_PET_LISTEN=100.122.32.106
|
||||
```
|
||||
|
||||
…and every `incus-pet deploy` binds there automatically.
|
||||
|
||||
### What the container's own firewall does
|
||||
|
||||
`container.nix` opens `8080` inside the container, so the proxy device's
|
||||
`connect=tcp:127.0.0.1:8080` reaches the service. The container has its
|
||||
own veth on `incusbr0`; nothing on the host's interfaces is involved
|
||||
until traffic hits the proxy device on `<listen-ip>:<host-port>`.
|
||||
|
||||
## Operator flow
|
||||
|
||||
### First deploy of an app
|
||||
|
||||
```sh
|
||||
incus-pet deploy github:srid/anywhen --port 7700 --listen 100.122.32.106
|
||||
```
|
||||
|
||||
This:
|
||||
|
||||
1. Synthesises a marshaling flake under `~/.local/state/incus-pet/anywhen/`.
|
||||
2. Launches `images:nixos/25.05` as container `anywhen` (with
|
||||
`security.nesting=true` so `nixos-rebuild` works inside).
|
||||
3. Bootstraps sshd + your pubkey via `incus exec`.
|
||||
4. Runs `nixos-rebuild switch --target-host root@<container-ip>`.
|
||||
5. Records `--port` and `--listen` in container metadata.
|
||||
6. Adds the `web` proxy device.
|
||||
|
||||
### Subsequent deploys
|
||||
|
||||
```sh
|
||||
incus-pet deploy github:srid/anywhen # picks up new commits on the branch
|
||||
```
|
||||
|
||||
No flags needed — `--port` and `--listen` are read from metadata.
|
||||
|
||||
### Listing what's running
|
||||
|
||||
```sh
|
||||
incus-pet list
|
||||
```
|
||||
|
||||
Filters `incus list` to only containers tagged with
|
||||
`user.incus-pet.flake-ref`.
|
||||
|
||||
### Removing
|
||||
|
||||
```sh
|
||||
incus-pet rm anywhen
|
||||
```
|
||||
|
||||
Stops and deletes the container, removes the marshaling flake from
|
||||
`~/.local/state/incus-pet/anywhen/`. Container metadata goes with the
|
||||
container.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- **`incus not in PATH`** — the host needs the incus daemon installed
|
||||
and the operator's user in `incus-admin` (log out and back in after
|
||||
the group is added).
|
||||
- **`first deploy of <name> needs --port N`** — pass `--port`; it will
|
||||
be recorded in container metadata for next time.
|
||||
- **`container did not get an IPv4 address within 30s`** — usually
|
||||
means `incusbr0` didn't come up. Check `incus network list` and
|
||||
`systemctl status incus-preseed`.
|
||||
- **`Failed to start Firewall.` inside the container** — the launch
|
||||
flag `-c security.nesting=true` is supposed to handle this; if it
|
||||
recurs, see `../README.md` and lxc/incus#526.
|
||||
- **SSH fails after first deploy** — the bootstrap step writes
|
||||
`/etc/nixos/incus-pet-bootstrap.nix` and runs `nixos-rebuild` inside
|
||||
the container; if that fails, `incus exec <name> -- journalctl -xeu nixos-rebuild` is the place to look.
|
||||
|
||||
## State
|
||||
|
||||
| What | Where |
|
||||
|-------------------------------|-----------------------------------------------------------------------|
|
||||
| Marshaling flake + lock | `~/.local/state/incus-pet/<name>/{flake.nix, flake.lock}` |
|
||||
| Operator-chosen host port | `incus config get <name> user.incus-pet.host-port` |
|
||||
| Operator-chosen listen IP | `incus config get <name> user.incus-pet.listen` |
|
||||
| Source flake ref | `incus config get <name> user.incus-pet.flake-ref` |
|
||||
| Service data | Inside the container, per the app's `services.<app>.stateDir` |
|
||||
|
||||
The marshaling flake carries no app-specific values — only the
|
||||
assembly. App config lives entirely in the upstream flake's
|
||||
`nixosModules.incus`.
|
||||
136
modules/nixos/linux/incus/incus-pet/SKILL.md
Normal file
136
modules/nixos/linux/incus/incus-pet/SKILL.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
name: incus-pet
|
||||
description: 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".
|
||||
argument-hint: "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.nix` exposing `nixosModules.incus` per the contract
|
||||
- `service-module.nix` authoring `services.<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:
|
||||
|
||||
1. Verify the flake exposes `packages.<system>.default`. If not, stop —
|
||||
prerequisite is a package derivation with `meta.mainProgram`.
|
||||
|
||||
2. Verify `nixosModules.default` (the service module). If MISSING but the
|
||||
flake has `homeManagerModules.default` (kolu case):
|
||||
- Author `nix/nixos/module.nix` as a system NixOS module mirroring the
|
||||
home-manager surface — same options (`enable`, `package`, `host`,
|
||||
`port`, ...), emitting `systemd.services.<app>` instead of a
|
||||
home-manager unit.
|
||||
- Expose as `nixosModules.default` in flake outputs.
|
||||
|
||||
If MISSING entirely (no module at all), stop — prerequisite.
|
||||
|
||||
3. Add `nixosModules.incus` to outputs (fill in `<app>`):
|
||||
|
||||
```nix
|
||||
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
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
4. 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.
|
||||
|
||||
5. Smoke-test:
|
||||
|
||||
```
|
||||
nix flake check
|
||||
nix eval .#nixosModules.incus
|
||||
```
|
||||
|
||||
6. If upstream (PR mode), open the PR. Else commit.
|
||||
|
||||
7. 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 = true`
|
||||
- `incus.container.hostname = "<app>"` (`mkDefault` ok)
|
||||
- `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>.port` to anything other than `8080`
|
||||
- `networking.firewall.allowedTCPPorts` — `container.nix` opens 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 is `set` if present, else `add`.
|
||||
- Container metadata (`user.incus-pet.{host-port,listen,flake-ref}`) is
|
||||
the source of truth for the operator-chosen exposure. `incus-pet list`
|
||||
filters by the `flake-ref` key.
|
||||
- If `--port` is missing on the first deploy, the command fails before
|
||||
launching anything.
|
||||
- Network failures during `nixos-rebuild --target-host` leave the
|
||||
container running its previous generation — `nixos-rebuild switch` is
|
||||
itself atomic.
|
||||
346
modules/nixos/linux/incus/incus-pet/default.nix
Normal file
346
modules/nixos/linux/incus/incus-pet/default.nix
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
# incus-pet — per-app incus container CLI ("pet PaaS").
|
||||
#
|
||||
# Subcommands: deploy, list, rm.
|
||||
#
|
||||
# Container port is fixed at 8080 (declared by ../container.nix); the
|
||||
# host-side <listen-ip>:<host-port> is unique per app and stored in
|
||||
# container metadata so re-deploys are flagless. See ./README.md for
|
||||
# the network model and operator flow.
|
||||
{ writeShellApplication
|
||||
, nix
|
||||
, nixos-rebuild
|
||||
, openssh
|
||||
, jq
|
||||
, coreutils
|
||||
, gnused
|
||||
, ...
|
||||
}:
|
||||
let
|
||||
# The container essentials module — baked in at build time as a store
|
||||
# path. The marshaling flake imports it directly so we don't have to
|
||||
# turn the incus/ tree into a flake.
|
||||
essentialsModule = ../container.nix;
|
||||
in
|
||||
writeShellApplication {
|
||||
name = "incus-pet";
|
||||
|
||||
runtimeInputs = [ nix nixos-rebuild openssh jq coreutils gnused ];
|
||||
|
||||
meta.description = "Deploy a NixOS service flake into a per-app incus container (pet PaaS).";
|
||||
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
|
||||
ESSENTIALS_MODULE='${essentialsModule}'
|
||||
CONTAINER_PORT=8080
|
||||
STATE_ROOT="''${XDG_STATE_HOME:-$HOME/.local/state}/incus-pet"
|
||||
INCUS_IMAGE="images:nixos/25.05"
|
||||
|
||||
log() { printf '[incus-pet] %s\n' "$*" >&2; }
|
||||
die() { log "error: $*"; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
incus-pet deploy <flake-ref> [<name>] [--port N] [--listen IP]
|
||||
incus-pet list
|
||||
incus-pet rm <name>
|
||||
|
||||
Environment:
|
||||
INCUS_PET_LISTEN default listen IP for the host-side proxy device
|
||||
(overridden by --listen)
|
||||
|
||||
First-time `deploy` requires --port; subsequent deploys read it from
|
||||
container metadata. See README.md for the network model.
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# ----- helpers -----
|
||||
|
||||
ensure_incus() {
|
||||
command -v incus >/dev/null 2>&1 || die "incus not in PATH (host needs the incus daemon)"
|
||||
}
|
||||
|
||||
operator_pubkey() {
|
||||
# The key root@<container> will trust. Try ed25519 then rsa.
|
||||
local f
|
||||
for f in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do
|
||||
if [ -r "$f" ]; then cat "$f"; return; fi
|
||||
done
|
||||
die "no ssh public key found at ~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub"
|
||||
}
|
||||
|
||||
container_exists() {
|
||||
incus info "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
container_ipv4() {
|
||||
# Poll briefly — fresh containers take a second to get DHCP.
|
||||
local name="$1" tries=0 ip=""
|
||||
while [ "$tries" -lt 30 ]; do
|
||||
ip=$(incus list "$name" --format=json 2>/dev/null \
|
||||
| jq -r '.[0].state.network.eth0.addresses[]? | select(.family=="inet") | .address' \
|
||||
| head -n1 || true)
|
||||
if [ -n "$ip" ] && [ "$ip" != "null" ]; then
|
||||
printf '%s\n' "$ip"; return 0
|
||||
fi
|
||||
sleep 1
|
||||
tries=$((tries + 1))
|
||||
done
|
||||
die "container $name did not get an IPv4 address within 30s"
|
||||
}
|
||||
|
||||
config_get() {
|
||||
# Echo empty string if the key is unset (incus returns empty + 0).
|
||||
incus config get "$1" "$2" 2>/dev/null || true
|
||||
}
|
||||
|
||||
write_marshaling_flake() {
|
||||
local name="$1" flake_ref="$2" pubkey="$3"
|
||||
local dir="$STATE_ROOT/$name"
|
||||
mkdir -p "$dir"
|
||||
|
||||
# Copy the essentials module into the marshaling flake dir as
|
||||
# ./container.nix and import it via relative path — keeps the
|
||||
# generated flake pure-evaluable (pure-eval forbids absolute
|
||||
# /nix/store imports not declared as flake inputs).
|
||||
cp -f "$ESSENTIALS_MODULE" "$dir/container.nix"
|
||||
|
||||
cat > "$dir/flake.nix" <<EOF
|
||||
# Generated by incus-pet — do not edit. Re-run \`incus-pet deploy\` to refresh.
|
||||
{
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
inputs.target.url = "$flake_ref";
|
||||
outputs = { nixpkgs, target, ... }: {
|
||||
nixosConfigurations."$name" = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
./container.nix
|
||||
target.nixosModules.incus
|
||||
({ ... }: {
|
||||
users.users.root.openssh.authorizedKeys.keys = [
|
||||
"$pubkey"
|
||||
];
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
EOF
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
precheck_target_flake() {
|
||||
local flake_ref="$1"
|
||||
if ! nix eval "$flake_ref#nixosModules.incus" --apply 'm: true' >/dev/null 2>&1; then
|
||||
die "flake $flake_ref does not expose nixosModules.incus; see SKILL.md Branch C for how to add the contract"
|
||||
fi
|
||||
}
|
||||
|
||||
eval_hostname() {
|
||||
local dir="$1" name="$2"
|
||||
nix eval --raw \
|
||||
"$dir#nixosConfigurations.\"$name\".config.incus.container.hostname"
|
||||
}
|
||||
|
||||
bootstrap_container() {
|
||||
# First-time bootstrap: install our pubkey, then enable sshd +
|
||||
# flakes via an in-container nixos-rebuild so the upcoming
|
||||
# --target-host rebuild has something to ssh into.
|
||||
local name="$1" pubkey="$2"
|
||||
|
||||
# The pubkey push is always idempotent (`tee` overwrites). Run it
|
||||
# every deploy so a rotated operator key gets picked up.
|
||||
log "ensuring authorized_keys on $name"
|
||||
incus exec "$name" -- mkdir -p /root/.ssh
|
||||
incus exec "$name" -- chmod 700 /root/.ssh
|
||||
printf '%s\n' "$pubkey" | incus exec "$name" -- tee /root/.ssh/authorized_keys >/dev/null
|
||||
incus exec "$name" -- chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
# The sshd+flakes bootstrap is one-shot; skip if the marker is
|
||||
# already in place (any subsequent deploy goes via --target-host).
|
||||
if incus exec "$name" -- test -f /etc/nixos/incus-pet-bootstrap.nix 2>/dev/null; then
|
||||
log "$name already bootstrapped; skipping sshd/flakes setup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "first-time bootstrap on $name (sshd + flakes)"
|
||||
|
||||
incus exec "$name" -- sh -c 'cat > /etc/nixos/incus-pet-bootstrap.nix' <<'NIX'
|
||||
{ ... }: {
|
||||
services.openssh.enable = true;
|
||||
services.openssh.settings.PermitRootLogin = "yes";
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
}
|
||||
NIX
|
||||
|
||||
# The official NixOS incus image's configuration.nix shape is
|
||||
# { modulesPath, ... }: { imports = [ "''${modulesPath}/..." ]; ... }
|
||||
# — sed prepends our import to that list. Reliable in practice;
|
||||
# fail loudly if the shape ever changes upstream.
|
||||
if ! incus exec "$name" -- grep -q 'imports = \[' /etc/nixos/configuration.nix; then
|
||||
die "$name: /etc/nixos/configuration.nix has no 'imports = [' line — incus image shape changed?"
|
||||
fi
|
||||
incus exec "$name" -- sed -i \
|
||||
's|imports = \[|imports = [ ./incus-pet-bootstrap.nix|' \
|
||||
/etc/nixos/configuration.nix
|
||||
|
||||
incus exec "$name" -- nixos-rebuild switch
|
||||
}
|
||||
|
||||
wire_proxy_device() {
|
||||
local name="$1" listen="$2" host_port="$3"
|
||||
local devname="web"
|
||||
|
||||
if incus config device show "$name" 2>/dev/null | grep -q "^$devname:"; then
|
||||
log "refreshing proxy device $devname on $name → $listen:$host_port"
|
||||
incus config device set "$name" "$devname" \
|
||||
listen="tcp:$listen:$host_port" \
|
||||
connect="tcp:127.0.0.1:$CONTAINER_PORT"
|
||||
else
|
||||
log "adding proxy device $devname on $name → $listen:$host_port"
|
||||
incus config device add "$name" "$devname" proxy \
|
||||
listen="tcp:$listen:$host_port" \
|
||||
connect="tcp:127.0.0.1:$CONTAINER_PORT"
|
||||
fi
|
||||
}
|
||||
|
||||
# ----- subcommands -----
|
||||
|
||||
cmd_deploy() {
|
||||
local flake_ref="" name="" port="" listen=""
|
||||
|
||||
[ $# -ge 1 ] || usage
|
||||
flake_ref="$1"; shift
|
||||
|
||||
# Optional positional name (anything that doesn't start with '-').
|
||||
if [ $# -ge 1 ] && [ "''${1#-}" = "$1" ]; then
|
||||
name="$1"; shift
|
||||
fi
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--port) port="$2"; shift 2 ;;
|
||||
--listen) listen="$2"; shift 2 ;;
|
||||
*) die "unknown flag: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_incus
|
||||
|
||||
# Normalize `.` to an absolute path so nix flake commands work
|
||||
# from anywhere downstream.
|
||||
if [ "$flake_ref" = "." ]; then
|
||||
flake_ref="path:$(pwd)"
|
||||
fi
|
||||
|
||||
precheck_target_flake "$flake_ref"
|
||||
|
||||
local pubkey
|
||||
pubkey=$(operator_pubkey)
|
||||
|
||||
# We need the hostname to know the container name, but the hostname
|
||||
# lives in the evaluated config — which means we need to write the
|
||||
# marshaling flake first. If --name wasn't passed, use a temporary
|
||||
# placeholder; we'll re-evaluate after.
|
||||
local tentative_name="''${name:-_probe}"
|
||||
local dir
|
||||
dir=$(write_marshaling_flake "$tentative_name" "$flake_ref" "$pubkey")
|
||||
|
||||
log "locking marshaling flake at $dir"
|
||||
( cd "$dir" && nix flake lock )
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
name=$(eval_hostname "$dir" "_probe")
|
||||
log "container name from flake: $name"
|
||||
# Re-emit the marshaling flake under the real name (the
|
||||
# nixosConfigurations attr key must match `--flake .#<name>`).
|
||||
rm -rf "$STATE_ROOT/_probe"
|
||||
dir=$(write_marshaling_flake "$name" "$flake_ref" "$pubkey")
|
||||
( cd "$dir" && nix flake lock )
|
||||
fi
|
||||
|
||||
# Resolve host port (CLI flag > container metadata > error).
|
||||
if [ -z "$port" ]; then
|
||||
if container_exists "$name"; then
|
||||
port=$(config_get "$name" user.incus-pet.host-port)
|
||||
fi
|
||||
[ -n "$port" ] || die "first deploy of $name needs --port N (host-side port)"
|
||||
fi
|
||||
|
||||
# Resolve listen IP (CLI flag > env > container metadata > 0.0.0.0).
|
||||
if [ -z "$listen" ]; then
|
||||
listen="''${INCUS_PET_LISTEN:-}"
|
||||
fi
|
||||
if [ -z "$listen" ] && container_exists "$name"; then
|
||||
listen=$(config_get "$name" user.incus-pet.listen)
|
||||
fi
|
||||
: "''${listen:=0.0.0.0}"
|
||||
|
||||
# Ensure container exists.
|
||||
if ! container_exists "$name"; then
|
||||
log "launching container $name from $INCUS_IMAGE"
|
||||
incus launch "$INCUS_IMAGE" "$name" -c security.nesting=true
|
||||
fi
|
||||
|
||||
local ip
|
||||
ip=$(container_ipv4 "$name")
|
||||
log "container $name has ipv4 $ip"
|
||||
|
||||
bootstrap_container "$name" "$pubkey"
|
||||
|
||||
log "activating: nixos-rebuild switch --flake $dir#$name --target-host root@$ip"
|
||||
( cd "$dir" && nixos-rebuild switch \
|
||||
--flake ".#$name" \
|
||||
--target-host "root@$ip" \
|
||||
--use-substitutes )
|
||||
|
||||
# Persist host-port + listen IP for next deploy.
|
||||
incus config set "$name" user.incus-pet.host-port "$port"
|
||||
incus config set "$name" user.incus-pet.listen "$listen"
|
||||
incus config set "$name" user.incus-pet.flake-ref "$flake_ref"
|
||||
|
||||
wire_proxy_device "$name" "$listen" "$port"
|
||||
|
||||
log "done: $name exposed on $listen:$port → container:8080"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
ensure_incus
|
||||
printf '%-20s %-50s %-25s %-10s\n' "NAME" "FLAKE" "EXPOSED" "STATUS"
|
||||
incus list --format=json | jq -r '.[] | select(.config["user.incus-pet.flake-ref"]) |
|
||||
[
|
||||
.name,
|
||||
.config["user.incus-pet.flake-ref"],
|
||||
(.config["user.incus-pet.listen"] + ":" + .config["user.incus-pet.host-port"]),
|
||||
.status
|
||||
] | @tsv' | while IFS=$'\t' read -r n f e s; do
|
||||
printf '%-20s %-50s %-25s %-10s\n' "$n" "$f" "$e" "$s"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_rm() {
|
||||
ensure_incus
|
||||
[ $# -eq 1 ] || die "rm takes exactly one argument: the container name"
|
||||
local name="$1"
|
||||
log "stopping + deleting container $name"
|
||||
incus stop "$name" --force 2>/dev/null || true
|
||||
incus delete "$name" 2>/dev/null || true
|
||||
rm -rf "''${STATE_ROOT:?}/$name"
|
||||
log "removed $name"
|
||||
}
|
||||
|
||||
# ----- dispatch -----
|
||||
|
||||
[ $# -ge 1 ] || usage
|
||||
sub="$1"; shift
|
||||
case "$sub" in
|
||||
deploy) cmd_deploy "$@" ;;
|
||||
list) cmd_list "$@" ;;
|
||||
rm) cmd_rm "$@" ;;
|
||||
-h|--help) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
'';
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Hosting webapps on home-server
|
||||
|
||||
Host them on `pureintent` (home-server)
|
||||
|
||||
Run nginx on `gate` (Hetzner VPS).
|
||||
|
||||
Put the two in a Tailscale network. Profit!
|
||||
|
||||
WARNING: This is not cleanly designed yet, so don't use it as a reference.
|
||||
|
||||
NOTE: This model would be made redundant/simplified after https://github.com/tailscale/tailscale/issues/11563
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{ flake, system, ... }:
|
||||
|
||||
# By convention, ports 15000 and above are used for webapps
|
||||
{
|
||||
/* actualism-app = rec {
|
||||
port = 15001;
|
||||
domain = "actualism.app";
|
||||
package = flake.inputs.actualism-app.packages.${system}.default;
|
||||
exec = "${package}/bin/actualism-app ${builtins.toString port}";
|
||||
};
|
||||
*/
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Configuration for the host on which all webapps will run.
|
||||
{ flake, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
webapps = import ./. { inherit flake; system = pkgs.stdenv.hostPlatform.system; };
|
||||
in
|
||||
{
|
||||
# Run each web app as a systemd service decided inside a container.
|
||||
containers = lib.mapAttrs
|
||||
(name: v: {
|
||||
autoStart = true;
|
||||
config = {
|
||||
systemd.services.${name} = {
|
||||
description = name;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = v.exec;
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
};
|
||||
})
|
||||
webapps;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Configuration for the VPS running nginx reverse proxy
|
||||
{ flake, pkgs, lib, webapps, ... }:
|
||||
|
||||
let
|
||||
host = "pureintent"; # See host.nix
|
||||
webapps = import ./. { inherit flake; system = pkgs.stdenv.hostPlatform.system; };
|
||||
in
|
||||
{
|
||||
services.tailscale.enable = true;
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedTlsSettings = true;
|
||||
|
||||
virtualHosts = lib.mapAttrs'
|
||||
(name: v: lib.nameValuePair v.domain {
|
||||
locations."/".proxyPass = "http://${host}:${builtins.toString v.port}";
|
||||
enableACME = true;
|
||||
addSSL = true;
|
||||
})
|
||||
webapps;
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = "srid@srid.ca";
|
||||
};
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 22 ];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue