split netty configuration.nix into per-service modules, remove sandbox-agent

Break the monolithic 495-line configuration.nix into focused modules:
- forgejo.nix: Forgejo service, git user, mirror sync timer
- betternas.nix: control-plane + node agent services
- vaultwarden.nix: Vaultwarden service
- nginx.nix: ACME + all Nginx virtualHosts

Remove sandbox-agent entirely (service, CORS proxy, package).
Keep netty.harivan.sh vhost reserved for future use.
This commit is contained in:
Harivansh Rathi 2026-04-01 23:07:53 -04:00
parent c97726766a
commit fd79908ad2
8 changed files with 297 additions and 4260 deletions

View file

@ -34,4 +34,4 @@ The VPS has a declarative service bundle:
- services only listen on 127.0.0.1 (runs behind nginx with ACME)
- Self hosts Forgejo mirroring to GitHub (git.harivan.sh)
- Self hosts VaultWarden
- Runs sandbox agent behind a CORS proxy
- betterNAS control-plane and node agent (api.betternas.com)

68
hosts/netty/betternas.nix Normal file
View file

@ -0,0 +1,68 @@
{
pkgs,
username,
...
}:
let
betternasDomain = "api.betternas.com";
betternasRepoDir = "/home/${username}/Documents/GitHub/betterNAS/betterNAS";
betternasNodeEnvFile = "/var/lib/betternas/node-agent/node-agent.env";
betternasNodeBinary = "${betternasRepoDir}/apps/node-agent/dist/betternas-node";
betternasNodeExportPath = "/home/${username}/Documents";
betternasNodeEnvCheck = pkgs.writeShellScript "betternas-node-env-check" ''
[ -f "${betternasNodeEnvFile}" ] && [ -x "${betternasNodeBinary}" ] && [ -d "${betternasNodeExportPath}" ]
'';
in
{
systemd.tmpfiles.rules = [
"d /var/lib/betternas/node-agent 0750 ${username} users -"
"z ${betternasNodeEnvFile} 0600 ${username} users -"
];
systemd.services.betternas-control-plane = {
description = "betterNAS Control Plane";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/var/lib/betternas/control-plane";
ExecStart = "${betternasRepoDir}/apps/control-plane/dist/control-plane";
EnvironmentFile = "/var/lib/betternas/control-plane/control-plane.env";
Restart = "on-failure";
RestartSec = 5;
StateDirectory = "betternas/control-plane";
};
};
systemd.services.betternas-node = {
description = "betterNAS Node";
after = [
"betternas-control-plane.service"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
PORT = "8090";
BETTERNAS_CONTROL_PLANE_URL = "http://127.0.0.1:3100";
BETTERNAS_NODE_DIRECT_ADDRESS = "https://${betternasDomain}";
BETTERNAS_EXPORT_PATH = betternasNodeExportPath;
BETTERNAS_NODE_DISPLAY_NAME = "netty";
};
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/var/lib/betternas/node-agent";
ExecCondition = betternasNodeEnvCheck;
ExecStart = betternasNodeBinary;
EnvironmentFile = "-${betternasNodeEnvFile}";
Restart = "on-failure";
RestartSec = 5;
StateDirectory = "betternas/node-agent";
};
};
}

View file

@ -9,103 +9,15 @@
}:
let
packageSets = import ../../lib/package-sets.nix { inherit inputs lib pkgs; };
sandboxDomain = "netty.harivan.sh";
forgejoDomain = "git.harivan.sh";
vaultDomain = "vault.harivan.sh";
betternasDomain = "api.betternas.com";
forgejoApiUrl = "http://127.0.0.1:19300";
betternasRepoDir = "/home/${username}/Documents/GitHub/betterNAS/betterNAS";
betternasNodeEnvFile = "/var/lib/betternas/node-agent/node-agent.env";
betternasNodeBinary = "${betternasRepoDir}/apps/node-agent/dist/betternas-node";
betternasNodeExportPath = "/home/${username}/Documents";
sandboxAgentPackage = pkgs.callPackage ../../pkgs/sandbox-agent { };
sandboxAgentDir = "/home/${username}/.config/sandbox-agent";
sandboxAgentPath =
packageSets.core
++ packageSets.extras
++ [
pkgs.bubblewrap
pkgs.git
pkgs.nodejs
pkgs.pnpm
sandboxAgentPackage
];
sandboxAgentEnvCheck = pkgs.writeShellScript "sandbox-agent-env-check" ''
[ -f "${sandboxAgentDir}/agent.env" ] && [ -f "${sandboxAgentDir}/public.env" ]
'';
betternasNodeEnvCheck = pkgs.writeShellScript "betternas-node-env-check" ''
[ -f "${betternasNodeEnvFile}" ] && [ -x "${betternasNodeBinary}" ] && [ -d "${betternasNodeExportPath}" ]
'';
sandboxAgentWrapper = pkgs.writeShellScript "sandbox-agent-public" ''
set -euo pipefail
set -a
. "${sandboxAgentDir}/public.env"
. "${sandboxAgentDir}/agent.env"
set +a
exec sandbox-agent server \
--host 127.0.0.1 \
--port "''${SANDBOX_AGENT_PORT}" \
--token "''${SANDBOX_AGENT_TOKEN}"
'';
sandboxCorsProxy = pkgs.writeText "sandbox-agent-cors-proxy.mjs" ''
import http from "node:http";
const listenHost = "127.0.0.1";
const listenPort = 2468;
const targetHost = "127.0.0.1";
const targetPort = 2470;
function setCorsHeaders(headers, req) {
headers["access-control-allow-origin"] = "*";
headers["access-control-allow-methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS";
headers["access-control-allow-headers"] =
req.headers["access-control-request-headers"] || "authorization,content-type";
headers["access-control-max-age"] = "86400";
return headers;
}
const server = http.createServer((req, res) => {
if (req.method === "OPTIONS") {
res.writeHead(204, setCorsHeaders({}, req));
res.end();
return;
}
const proxyReq = http.request(
{
host: targetHost,
port: targetPort,
method: req.method,
path: req.url,
headers: {
...req.headers,
host: `''${targetHost}:''${targetPort}`,
},
},
(proxyRes) => {
res.writeHead(
proxyRes.statusCode || 502,
setCorsHeaders({ ...proxyRes.headers }, req),
);
proxyRes.pipe(res);
},
);
proxyReq.on("error", () => {
res.writeHead(502, setCorsHeaders({ "content-type": "text/plain" }, req));
res.end("Upstream request failed");
});
req.pipe(proxyReq);
});
server.listen(listenPort, listenHost);
'';
in
{
imports = [
./hardware-configuration.nix
./disk-config.nix
./nginx.nix
./vaultwarden.nix
./forgejo.nix
./betternas.nix
../../modules/base.nix
(modulesPath + "/profiles/minimal.nix")
(modulesPath + "/profiles/headless.nix")
@ -196,300 +108,9 @@ in
virtualisation.docker.enable = true;
environment.systemPackages = packageSets.extras ++ [
pkgs.bubblewrap
pkgs.pnpm
pkgs.nodejs
pkgs.php
sandboxAgentPackage
];
systemd.tmpfiles.rules = [
"L /usr/bin/bwrap - - - - ${pkgs.bubblewrap}/bin/bwrap"
"d /var/lib/betternas/node-agent 0750 ${username} users -"
"z ${betternasNodeEnvFile} 0600 ${username} users -"
"z /var/lib/vaultwarden/vaultwarden.env 0600 vaultwarden vaultwarden -"
];
# --- ACME / Let's Encrypt ---
security.acme = {
acceptTerms = true;
defaults.email = "rathiharivansh@gmail.com";
};
# --- Nginx reverse proxy ---
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
clientMaxBodySize = "512m";
virtualHosts.${sandboxDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:2470";
};
virtualHosts.${forgejoDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:19300";
};
virtualHosts.${vaultDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:8222";
};
virtualHosts.${betternasDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:3100";
locations."/dav/".proxyPass = "http://127.0.0.1:8090";
};
};
# --- Vaultwarden ---
services.vaultwarden = {
enable = true;
backupDir = "/var/backup/vaultwarden";
environmentFile = "/var/lib/vaultwarden/vaultwarden.env";
config = {
DOMAIN = "https://${vaultDomain}";
SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222;
};
};
# --- Forgejo ---
users.users.git = {
isSystemUser = true;
home = "/var/lib/forgejo";
group = "git";
shell = "${pkgs.bash}/bin/bash";
};
users.groups.git = { };
services.forgejo = {
enable = true;
user = "git";
group = "git";
settings = {
repository = {
FORCE_PRIVATE = true;
DEFAULT_PRIVATE = "private";
DEFAULT_PUSH_CREATE_PRIVATE = true;
};
server = {
DOMAIN = forgejoDomain;
ROOT_URL = "https://${forgejoDomain}/";
HTTP_PORT = 19300;
SSH_DOMAIN = forgejoDomain;
};
service = {
DISABLE_REGISTRATION = true;
REQUIRE_SIGNIN_VIEW = true;
};
session.COOKIE_SECURE = true;
mirror = {
DEFAULT_INTERVAL = "1h";
MIN_INTERVAL = "10m";
};
};
};
# --- Forgejo mirror sync (hourly) ---
systemd.services.forgejo-mirror-sync = {
description = "Sync GitHub mirrors to Forgejo";
after = [ "forgejo.service" ];
requires = [ "forgejo.service" ];
serviceConfig = {
Type = "oneshot";
EnvironmentFile = "/etc/forgejo-mirror.env";
};
path = [
pkgs.curl
pkgs.jq
pkgs.coreutils
pkgs.gnused
];
script = ''
set -euo pipefail
api_call() {
local response http_code body
response=$(curl -sS -w "\n%{http_code}" "$@")
http_code=$(printf '%s\n' "$response" | tail -n1)
body=$(printf '%s\n' "$response" | sed '$d')
if [ "$http_code" -ge 400 ]; then
printf '[forgejo-mirror-sync] HTTP %s\n' "$http_code" >&2
printf '%s\n' "$body" >&2
return 1
fi
printf '%s' "$body"
}
gh_user=$(api_call -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user" | jq -r '.login')
repos_file=$(mktemp)
trap 'rm -f "$repos_file"' EXIT
page=1
while true; do
batch=$(api_call -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user/repos?per_page=100&page=$page&visibility=all&affiliation=owner,organization_member")
count=$(printf '%s' "$batch" | jq length)
[ "$count" -eq 0 ] && break
printf '%s' "$batch" | jq -r '.[] | [.full_name, .clone_url] | @tsv' >> "$repos_file"
page=$((page + 1))
done
sort -u "$repos_file" -o "$repos_file"
while IFS=$'\t' read -r full_name clone_url; do
repo_owner="''${full_name%%/*}"
repo_name="''${full_name#*/}"
if [ "$repo_owner" = "$gh_user" ]; then
forgejo_repo_name="$repo_name"
else
forgejo_repo_name="$repo_owner--$repo_name"
fi
status=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: token $FORGEJO_TOKEN" \
"${forgejoApiUrl}/api/v1/repos/$FORGEJO_OWNER/$forgejo_repo_name" || true)
if [ "$status" = "404" ]; then
api_call -X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
"${forgejoApiUrl}/api/v1/repos/migrate" \
-d "$(jq -n \
--arg addr "$clone_url" \
--arg name "$forgejo_repo_name" \
--arg owner "$FORGEJO_OWNER" \
--arg token "$GITHUB_TOKEN" \
'{
clone_addr: $addr,
repo_name: $name,
repo_owner: $owner,
private: true,
mirror: true,
auth_token: $token,
service: "github"
}')" \
> /dev/null
echo "Created mirror: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name"
else
if ! api_call -X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
"${forgejoApiUrl}/api/v1/repos/$FORGEJO_OWNER/$forgejo_repo_name/mirror-sync" \
> /dev/null; then
echo "Failed mirror sync: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name" >&2
continue
fi
echo "Synced mirror: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name"
fi
done < "$repos_file"
'';
};
systemd.timers.forgejo-mirror-sync = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "hourly";
Persistent = true;
RandomizedDelaySec = "5m";
};
};
# --- Sandbox Agent (declarative systemd services) ---
systemd.services.sandbox-agent = {
description = "Sandbox Agent";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = sandboxAgentPath;
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/home/${username}";
ExecCondition = sandboxAgentEnvCheck;
ExecStart = sandboxAgentWrapper;
Restart = "on-failure";
RestartSec = 5;
};
};
systemd.services.sandbox-cors-proxy = {
description = "Sandbox CORS Proxy";
after = [ "sandbox-agent.service" ];
requires = [ "sandbox-agent.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/home/${username}";
ExecCondition = sandboxAgentEnvCheck;
ExecStart = "${pkgs.nodejs}/bin/node ${sandboxCorsProxy}";
Restart = "on-failure";
RestartSec = 5;
};
};
# --- betterNAS control-plane ---
systemd.services.betternas-control-plane = {
description = "betterNAS Control Plane";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/var/lib/betternas/control-plane";
ExecStart = "/home/${username}/Documents/GitHub/betterNAS/betterNAS/apps/control-plane/dist/control-plane";
EnvironmentFile = "/var/lib/betternas/control-plane/control-plane.env";
Restart = "on-failure";
RestartSec = 5;
StateDirectory = "betternas/control-plane";
};
};
systemd.services.betternas-node = {
description = "betterNAS Node";
after = [
"betternas-control-plane.service"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
PORT = "8090";
BETTERNAS_CONTROL_PLANE_URL = "http://127.0.0.1:3100";
BETTERNAS_NODE_DIRECT_ADDRESS = "https://${betternasDomain}";
BETTERNAS_EXPORT_PATH = betternasNodeExportPath;
BETTERNAS_NODE_DISPLAY_NAME = "netty";
};
serviceConfig = {
Type = "simple";
User = username;
Group = "users";
WorkingDirectory = "/var/lib/betternas/node-agent";
ExecCondition = betternasNodeEnvCheck;
ExecStart = betternasNodeBinary;
EnvironmentFile = "-${betternasNodeEnvFile}";
Restart = "on-failure";
RestartSec = 5;
StateDirectory = "betternas/node-agent";
};
};
system.configurationRevision = self.rev or self.dirtyRev or null;
system.stateVersion = "24.11";
}

153
hosts/netty/forgejo.nix Normal file
View file

@ -0,0 +1,153 @@
{
pkgs,
username,
...
}:
let
forgejoDomain = "git.harivan.sh";
forgejoApiUrl = "http://127.0.0.1:19300";
in
{
users.users.git = {
isSystemUser = true;
home = "/var/lib/forgejo";
group = "git";
shell = "${pkgs.bash}/bin/bash";
};
users.groups.git = { };
services.forgejo = {
enable = true;
user = "git";
group = "git";
settings = {
repository = {
FORCE_PRIVATE = true;
DEFAULT_PRIVATE = "private";
DEFAULT_PUSH_CREATE_PRIVATE = true;
};
server = {
DOMAIN = forgejoDomain;
ROOT_URL = "https://${forgejoDomain}/";
HTTP_PORT = 19300;
SSH_DOMAIN = forgejoDomain;
};
service = {
DISABLE_REGISTRATION = true;
REQUIRE_SIGNIN_VIEW = true;
};
session.COOKIE_SECURE = true;
mirror = {
DEFAULT_INTERVAL = "1h";
MIN_INTERVAL = "10m";
};
};
};
# --- Forgejo mirror sync (hourly) ---
systemd.services.forgejo-mirror-sync = {
description = "Sync GitHub mirrors to Forgejo";
after = [ "forgejo.service" ];
requires = [ "forgejo.service" ];
serviceConfig = {
Type = "oneshot";
EnvironmentFile = "/etc/forgejo-mirror.env";
};
path = [
pkgs.curl
pkgs.jq
pkgs.coreutils
pkgs.gnused
];
script = ''
set -euo pipefail
api_call() {
local response http_code body
response=$(curl -sS -w "\n%{http_code}" "$@")
http_code=$(printf '%s\n' "$response" | tail -n1)
body=$(printf '%s\n' "$response" | sed '$d')
if [ "$http_code" -ge 400 ]; then
printf '[forgejo-mirror-sync] HTTP %s\n' "$http_code" >&2
printf '%s\n' "$body" >&2
return 1
fi
printf '%s' "$body"
}
gh_user=$(api_call -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user" | jq -r '.login')
repos_file=$(mktemp)
trap 'rm -f "$repos_file"' EXIT
page=1
while true; do
batch=$(api_call -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user/repos?per_page=100&page=$page&visibility=all&affiliation=owner,organization_member")
count=$(printf '%s' "$batch" | jq length)
[ "$count" -eq 0 ] && break
printf '%s' "$batch" | jq -r '.[] | [.full_name, .clone_url] | @tsv' >> "$repos_file"
page=$((page + 1))
done
sort -u "$repos_file" -o "$repos_file"
while IFS=$'\t' read -r full_name clone_url; do
repo_owner="''${full_name%%/*}"
repo_name="''${full_name#*/}"
if [ "$repo_owner" = "$gh_user" ]; then
forgejo_repo_name="$repo_name"
else
forgejo_repo_name="$repo_owner--$repo_name"
fi
status=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: token $FORGEJO_TOKEN" \
"${forgejoApiUrl}/api/v1/repos/$FORGEJO_OWNER/$forgejo_repo_name" || true)
if [ "$status" = "404" ]; then
api_call -X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
"${forgejoApiUrl}/api/v1/repos/migrate" \
-d "$(jq -n \
--arg addr "$clone_url" \
--arg name "$forgejo_repo_name" \
--arg owner "$FORGEJO_OWNER" \
--arg token "$GITHUB_TOKEN" \
'{
clone_addr: $addr,
repo_name: $name,
repo_owner: $owner,
private: true,
mirror: true,
auth_token: $token,
service: "github"
}')" \
> /dev/null
echo "Created mirror: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name"
else
if ! api_call -X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
"${forgejoApiUrl}/api/v1/repos/$FORGEJO_OWNER/$forgejo_repo_name/mirror-sync" \
> /dev/null; then
echo "Failed mirror sync: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name" >&2
continue
fi
echo "Synced mirror: $full_name -> $FORGEJO_OWNER/$forgejo_repo_name"
fi
done < "$repos_file"
'';
};
systemd.timers.forgejo-mirror-sync = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "hourly";
Persistent = true;
RandomizedDelaySec = "5m";
};
};
}

48
hosts/netty/nginx.nix Normal file
View file

@ -0,0 +1,48 @@
{
...
}:
let
sandboxDomain = "netty.harivan.sh";
forgejoDomain = "git.harivan.sh";
vaultDomain = "vault.harivan.sh";
betternasDomain = "api.betternas.com";
in
{
security.acme = {
acceptTerms = true;
defaults.email = "rathiharivansh@gmail.com";
};
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
clientMaxBodySize = "512m";
# Reserved for future use - nothing listening on this port yet
virtualHosts.${sandboxDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:2470";
};
virtualHosts.${forgejoDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:19300";
};
virtualHosts.${vaultDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:8222";
};
virtualHosts.${betternasDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:3100";
locations."/dav/".proxyPass = "http://127.0.0.1:8090";
};
};
}

View file

@ -0,0 +1,23 @@
{
...
}:
let
vaultDomain = "vault.harivan.sh";
in
{
systemd.tmpfiles.rules = [
"z /var/lib/vaultwarden/vaultwarden.env 0600 vaultwarden vaultwarden -"
];
services.vaultwarden = {
enable = true;
backupDir = "/var/backup/vaultwarden";
environmentFile = "/var/lib/vaultwarden/vaultwarden.env";
config = {
DOMAIN = "https://${vaultDomain}";
SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222;
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,44 +0,0 @@
{
lib,
fetchFromGitHub,
openssl,
pkg-config,
rustPlatform,
}:
rustPlatform.buildRustPackage {
pname = "sandbox-agent";
version = "0.5.0-rc.1";
src = fetchFromGitHub {
owner = "rivet-dev";
repo = "sandbox-agent";
rev = "v0.5.0-rc.1";
hash = "sha256-oeOpWjaQlQZZzwQGts4yJgL3STDCd3Hz2qbOJ4N2HBM=";
};
cargoLock.lockFile = ./Cargo.lock;
prePatch = ''
cp ${./Cargo.lock} Cargo.lock
'';
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl ];
cargoBuildFlags = [
"-p"
"sandbox-agent"
];
env.SANDBOX_AGENT_SKIP_INSPECTOR = "1";
doCheck = false;
meta = with lib; {
description = "Universal API for coding agents in sandboxes";
homepage = "https://sandboxagent.dev";
license = licenses.asl20;
mainProgram = "sandbox-agent";
platforms = platforms.unix;
};
}