Skip to content

Commit 2a9a3be

Browse files
committed
nixos: add openclaw-gateway module
Adds services.openclaw-gateway NixOS module: - systemd service for │ ◇ Doctor warnings ──────────────────────────────────────╮ │ │ │ - State dir migration skipped: target already exists │ │ (/Users/josh/.openclaw). Remove or merge manually. │ │ │ ├────────────────────────────────────────────────────────╯ - deep-merged config attrset -> /etc/openclaw/openclaw.json - configurable unit name, env, preStart, and PATH This lets downstream stacks (clawdinators) avoid re-implementing the gateway service.
1 parent 0eac830 commit 2a9a3be

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
)
121121
// {
122122
overlays.default = overlay;
123+
nixosModules.openclaw-gateway = import ./nix/modules/nixos/openclaw-gateway.nix;
123124
homeManagerModules.openclaw = import ./nix/modules/home-manager/openclaw.nix;
124125
darwinModules.openclaw = import ./nix/modules/darwin/openclaw.nix;
125126
};
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
8+
let
9+
cfg = config.services.openclaw-gateway;
10+
11+
deepConfigType = lib.types.mkOptionType {
12+
name = "openclaw-config-attrs";
13+
description = "OpenClaw JSON config (attrset), merged deeply via lib.recursiveUpdate.";
14+
check = builtins.isAttrs;
15+
merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs);
16+
};
17+
18+
configJson = builtins.toJSON cfg.config;
19+
generatedConfigFile = pkgs.writeText "openclaw.json" configJson;
20+
configFile = if cfg.configFile != null then cfg.configFile else generatedConfigFile;
21+
22+
# `environment.etc` takes a relative path.
23+
etcRelPath = lib.removePrefix "/etc/" cfg.configPath;
24+
25+
execStartCmd =
26+
if cfg.execStart != null then
27+
cfg.execStart
28+
else
29+
"${cfg.package}/bin/openclaw gateway --port ${toString cfg.port}";
30+
31+
in
32+
{
33+
options.services.openclaw-gateway = with lib; {
34+
enable = mkEnableOption "OpenClaw gateway (openclaw gateway as a systemd service)";
35+
36+
unitName = mkOption {
37+
type = types.str;
38+
default = "openclaw-gateway";
39+
description = "systemd unit name (service will be <unitName>.service).";
40+
};
41+
42+
package = mkOption {
43+
type = types.package;
44+
default = if pkgs ? openclaw-gateway then pkgs.openclaw-gateway else pkgs.openclaw;
45+
description = "OpenClaw gateway package.";
46+
};
47+
48+
port = mkOption {
49+
type = types.port;
50+
default = 18789;
51+
description = "Gateway listen port.";
52+
};
53+
54+
user = mkOption {
55+
type = types.str;
56+
default = "openclaw";
57+
description = "System user running the gateway.";
58+
};
59+
60+
group = mkOption {
61+
type = types.str;
62+
default = "openclaw";
63+
description = "System group running the gateway.";
64+
};
65+
66+
createUser = mkOption {
67+
type = types.bool;
68+
default = true;
69+
description = "Create the user/group automatically.";
70+
};
71+
72+
stateDir = mkOption {
73+
type = types.str;
74+
default = "/var/lib/openclaw";
75+
description = "State dir (OPENCLAW_STATE_DIR).";
76+
};
77+
78+
workingDirectory = mkOption {
79+
type = types.str;
80+
default = cfg.stateDir;
81+
description = "Working directory for the systemd service.";
82+
};
83+
84+
configPath = mkOption {
85+
type = types.str;
86+
default = "/etc/openclaw/openclaw.json";
87+
description = "Path to the OpenClaw JSON config file (OPENCLAW_CONFIG_PATH). Must be under /etc.";
88+
};
89+
90+
configFile = mkOption {
91+
type = types.nullOr types.path;
92+
default = null;
93+
description = "Optional path to an existing config file. If set, it is copied to configPath (under /etc).";
94+
};
95+
96+
config = mkOption {
97+
type = deepConfigType;
98+
default = { };
99+
description = "OpenClaw JSON config (attrset), deep-merged across definitions.";
100+
};
101+
102+
logPath = mkOption {
103+
type = types.str;
104+
default = "${cfg.stateDir}/logs/gateway.log";
105+
description = "Log file path (systemd StandardOutput/StandardError append).";
106+
};
107+
108+
environment = mkOption {
109+
type = types.attrsOf types.str;
110+
default = { };
111+
description = "Additional environment variables for the gateway process.";
112+
};
113+
114+
environmentFiles = mkOption {
115+
type = types.listOf types.str;
116+
default = [ ];
117+
description = "systemd EnvironmentFile= entries (use leading '-' to ignore missing).";
118+
};
119+
120+
execStart = mkOption {
121+
type = types.nullOr types.str;
122+
default = null;
123+
description = "Override ExecStart command. If unset, runs: openclaw gateway --port <port>.";
124+
};
125+
126+
execStartPre = mkOption {
127+
type = types.listOf types.str;
128+
default = [ ];
129+
description = "List of ExecStartPre= commands.";
130+
};
131+
132+
servicePath = mkOption {
133+
type = types.listOf types.package;
134+
default = [ ];
135+
description = "Extra packages added to systemd service PATH.";
136+
};
137+
138+
restart = mkOption {
139+
type = types.str;
140+
default = "always";
141+
description = "systemd Restart=.";
142+
};
143+
144+
restartSec = mkOption {
145+
type = types.int;
146+
default = 2;
147+
description = "systemd RestartSec=.";
148+
};
149+
};
150+
151+
config = lib.mkIf cfg.enable {
152+
assertions = [
153+
{
154+
assertion = lib.hasPrefix "/etc/" cfg.configPath;
155+
message = "services.openclaw-gateway.configPath must be under /etc (got: ${cfg.configPath}).";
156+
}
157+
];
158+
159+
users.groups.${cfg.group} = lib.mkIf cfg.createUser { };
160+
users.users.${cfg.user} = lib.mkIf cfg.createUser {
161+
isSystemUser = true;
162+
group = cfg.group;
163+
home = cfg.stateDir;
164+
createHome = true;
165+
shell = pkgs.bashInteractive;
166+
};
167+
168+
systemd.tmpfiles.rules = [
169+
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
170+
"d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -"
171+
"d ${builtins.dirOf cfg.configPath} 0755 root root - -"
172+
];
173+
174+
environment.etc.${etcRelPath} = {
175+
mode = "0644";
176+
source = configFile;
177+
};
178+
179+
systemd.services.${cfg.unitName} = {
180+
description = "OpenClaw gateway";
181+
wantedBy = [ "multi-user.target" ];
182+
after = [ "network.target" ];
183+
184+
environment = {
185+
OPENCLAW_CONFIG_PATH = cfg.configPath;
186+
OPENCLAW_STATE_DIR = cfg.stateDir;
187+
188+
# Backward-compatible env names.
189+
CLAWDBOT_CONFIG_PATH = cfg.configPath;
190+
CLAWDBOT_STATE_DIR = cfg.stateDir;
191+
}
192+
// cfg.environment;
193+
194+
serviceConfig = {
195+
User = cfg.user;
196+
Group = cfg.group;
197+
WorkingDirectory = cfg.workingDirectory;
198+
199+
EnvironmentFile = cfg.environmentFiles;
200+
ExecStartPre = cfg.execStartPre;
201+
ExecStart = execStartCmd;
202+
203+
Restart = cfg.restart;
204+
RestartSec = cfg.restartSec;
205+
206+
StandardOutput = "append:${cfg.logPath}";
207+
StandardError = "append:${cfg.logPath}";
208+
};
209+
210+
path = [
211+
pkgs.bash
212+
pkgs.coreutils
213+
]
214+
++ cfg.servicePath;
215+
};
216+
};
217+
}

0 commit comments

Comments
 (0)