-1
.envrc
-1
.envrc
···
1
-
eval "$(nix print-dev-env)"
+4
-4
README.md
+4
-4
README.md
···
251
251
Services are automatically backed up nightly using restic to Backblaze B2. The `atelier-backup` CLI provides an interactive TUI for managing backups:
252
252
253
253
```bash
254
-
atelier-backup # Interactive menu
255
-
atelier-backup status # Show backup status
256
-
atelier-backup restore # Restore wizard
257
-
atelier-backup dr # Disaster recovery
254
+
sudo atelier-backup # Interactive menu
255
+
sudo atelier-backup status # Show backup status
256
+
sudo atelier-backup restore # Restore wizard
257
+
sudo atelier-backup dr # Disaster recovery
258
258
```
259
259
260
260
See [modules/nixos/services/restic/README.md](modules/nixos/services/restic/README.md) for setup and usage.
+11
-4
machines/terebithia/default.nix
+11
-4
machines/terebithia/default.nix
···
221
221
];
222
222
allowedUDPPorts = [
223
223
28869 # Minecraft voice chat
224
+
19132 # mc geyser
224
225
];
225
226
logRefusedConnections = false;
226
227
rejectPackets = true;
···
379
380
380
381
# Backup configuration for tangled services
381
382
atelier.backup.services.knot = {
382
-
paths = [ "/home/git" ]; # Git repositories managed by knot
383
+
paths = [ "/home/git" ]; # Git repositories managed by knot
383
384
exclude = [ "*.log" ];
384
385
# Uses SQLite, stop before backup
385
386
preBackup = "systemctl stop knot";
···
388
389
389
390
atelier.backup.services.spindle = {
390
391
paths = [ "/var/lib/spindle" ];
391
-
exclude = [ "*.log" "cache/*" ];
392
+
exclude = [
393
+
"*.log"
394
+
"cache/*"
395
+
];
392
396
# Uses SQLite, stop before backup
393
397
preBackup = "systemctl stop spindle";
394
398
postBackup = "systemctl start spindle";
···
414
418
enable = true;
415
419
domain = "l4.dunkirk.sh";
416
420
port = 3004;
417
-
autoUpdate = false;
421
+
deploy.autoUpdate = false;
418
422
secretsFile = config.age.secrets.l4.path;
419
423
};
420
424
···
445
449
# Backup configuration for n8n
446
450
atelier.backup.services.n8n = {
447
451
paths = [ "/var/lib/n8n" ];
448
-
exclude = [ "*.log" "cache/*" ];
452
+
exclude = [
453
+
"*.log"
454
+
"cache/*"
455
+
];
449
456
# n8n uses SQLite, stop before backup
450
457
preBackup = "systemctl stop n8n";
451
458
postBackup = "systemctl start n8n";
+4
-18
modules/nixos/services/battleship-arena.nix
+4
-18
modules/nixos/services/battleship-arena.nix
···
56
56
description = "The battleship-arena package to use";
57
57
};
58
58
59
-
backup = {
60
-
enable = mkEnableOption "Enable backups for battleship-arena" // { default = true; };
61
59
62
-
paths = mkOption {
63
-
type = types.listOf types.str;
64
-
default = [ "/var/lib/battleship-arena" ];
65
-
description = "Paths to back up";
66
-
};
67
-
68
-
exclude = mkOption {
69
-
type = types.listOf types.str;
70
-
default = [ "*.log" ];
71
-
description = "Patterns to exclude from backup";
72
-
};
73
-
};
74
60
};
75
61
76
62
config = mkIf cfg.enable {
···
175
161
176
162
networking.firewall.allowedTCPPorts = [ cfg.sshPort ];
177
163
178
-
# Register backup configuration
179
-
atelier.backup.services.battleship-arena = mkIf cfg.backup.enable {
180
-
inherit (cfg.backup) paths exclude;
181
-
# Has SQLite database, stop before backup
164
+
# Register backup configuration (SQLite database)
165
+
atelier.backup.services.battleship-arena = {
166
+
paths = [ "/var/lib/battleship-arena" ];
167
+
exclude = [ "*.log" "battleship-engine" ];
182
168
preBackup = "systemctl stop battleship-arena";
183
169
postBackup = "systemctl start battleship-arena";
184
170
};
+1
-1
modules/nixos/services/cachet.nix
+1
-1
modules/nixos/services/cachet.nix
···
17
17
entryPoint = "src/index.ts";
18
18
19
19
extraConfig = cfg: {
20
-
# Set DATABASE_PATH environment variable
20
+
# Set DATABASE_PATH environment variable (uses the data dir created by mkService)
21
21
systemd.services.cachet.serviceConfig.Environment = [
22
22
"DATABASE_PATH=${cfg.dataDir}/data/cachet.db"
23
23
];
+14
-155
modules/nixos/services/emojibot.nix
+14
-155
modules/nixos/services/emojibot.nix
···
1
-
{
2
-
config,
3
-
lib,
4
-
pkgs,
5
-
...
6
-
}:
1
+
# Emojibot - Slack emoji management service
2
+
#
3
+
# Stateless service, no database backup needed
4
+
7
5
let
8
-
cfg = config.atelier.services.emojibot;
6
+
mkService = import ../../lib/mkService.nix;
9
7
in
10
-
{
11
-
options.atelier.services.emojibot = {
12
-
enable = lib.mkEnableOption "Emojibot Slack emoji management service";
13
8
14
-
domain = lib.mkOption {
15
-
type = lib.types.str;
16
-
description = "Domain to serve emojibot on";
17
-
};
9
+
mkService {
10
+
name = "emojibot";
11
+
description = "Emojibot Slack emoji management service";
12
+
defaultPort = 3002;
13
+
runtime = "bun";
14
+
entryPoint = "src/index.ts";
18
15
19
-
port = lib.mkOption {
20
-
type = lib.types.port;
21
-
default = 3002;
22
-
description = "Port to run emojibot on";
23
-
};
24
-
25
-
dataDir = lib.mkOption {
26
-
type = lib.types.path;
27
-
default = "/var/lib/emojibot";
28
-
description = "Directory to store emojibot data";
29
-
};
30
-
31
-
secretsFile = lib.mkOption {
32
-
type = lib.types.path;
33
-
description = ''
34
-
Path to secrets file containing:
35
-
- SLACK_SIGNING_SECRET
36
-
- SLACK_BOT_TOKEN
37
-
- SLACK_APP_TOKEN
38
-
- SLACK_BOT_USER_TOKEN (get from browser, see emojibot README)
39
-
- SLACK_COOKIE (get from browser, see emojibot README)
40
-
- SLACK_WORKSPACE (e.g. "myworkspace" for myworkspace.slack.com)
41
-
- SLACK_CHANNEL (channel ID where emojis are posted)
42
-
- ADMINS (comma-separated list of slack user IDs)
43
-
'';
44
-
};
45
-
46
-
repository = lib.mkOption {
47
-
type = lib.types.str;
48
-
default = "https://github.com/taciturnaxolotl/emojibot.git";
49
-
description = "Git repository URL (optional, for auto-deployment)";
50
-
};
51
-
52
-
autoUpdate = lib.mkEnableOption "Automatically git pull on service restart";
53
-
54
-
backup = {
55
-
enable = lib.mkEnableOption "Enable backups for emojibot" // { default = true; };
56
-
57
-
paths = lib.mkOption {
58
-
type = lib.types.listOf lib.types.str;
59
-
default = [ cfg.dataDir ];
60
-
description = "Paths to back up";
61
-
};
62
-
63
-
exclude = lib.mkOption {
64
-
type = lib.types.listOf lib.types.str;
65
-
default = [ "*.log" "app/.git" "app/node_modules" ];
66
-
description = "Patterns to exclude from backup";
67
-
};
68
-
};
69
-
};
70
-
71
-
config = lib.mkIf cfg.enable {
72
-
users.groups.services = { };
73
-
74
-
users.users.emojibot = {
75
-
isSystemUser = true;
76
-
group = "emojibot";
77
-
extraGroups = [ "services" ];
78
-
home = cfg.dataDir;
79
-
createHome = true;
80
-
shell = pkgs.bash;
81
-
};
82
-
83
-
users.groups.emojibot = { };
84
-
85
-
security.sudo.extraRules = [
86
-
{
87
-
users = [ "emojibot" ];
88
-
commands = [
89
-
{
90
-
command = "/run/current-system/sw/bin/systemctl restart emojibot.service";
91
-
options = [ "NOPASSWD" ];
92
-
}
93
-
];
94
-
}
95
-
];
96
-
97
-
systemd.services.emojibot = {
98
-
description = "Emojibot Slack emoji management service";
99
-
wantedBy = [ "multi-user.target" ];
100
-
after = [ "network.target" ];
101
-
path = [ pkgs.git ];
102
-
103
-
preStart = ''
104
-
if [ ! -d ${cfg.dataDir}/app/.git ]; then
105
-
${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app
106
-
fi
107
-
108
-
cd ${cfg.dataDir}/app
109
-
'' + lib.optionalString cfg.autoUpdate ''
110
-
${pkgs.git}/bin/git pull
111
-
'' + ''
112
-
113
-
if [ ! -f src/index.ts ]; then
114
-
echo "No code found at ${cfg.dataDir}/app/src/index.ts"
115
-
exit 1
116
-
fi
117
-
118
-
echo "Installing dependencies..."
119
-
${pkgs.unstable.bun}/bin/bun install
120
-
'';
121
-
122
-
serviceConfig = {
123
-
Type = "simple";
124
-
User = "emojibot";
125
-
Group = "emojibot";
126
-
EnvironmentFile = cfg.secretsFile;
127
-
Environment = [
128
-
"NODE_ENV=production"
129
-
"PORT=${toString cfg.port}"
130
-
];
131
-
ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${cfg.dataDir}/app && ${pkgs.unstable.bun}/bin/bun run src/index.ts'";
132
-
Restart = "always";
133
-
RestartSec = "10s";
134
-
};
135
-
136
-
serviceConfig.ExecStartPre = [
137
-
"+${pkgs.writeShellScript "emojibot-setup" ''
138
-
mkdir -p ${cfg.dataDir}/app
139
-
chown -R emojibot:services ${cfg.dataDir}
140
-
chmod -R g+rwX ${cfg.dataDir}
141
-
''}"
142
-
];
143
-
};
144
-
145
-
services.caddy.virtualHosts.${cfg.domain} = {
146
-
extraConfig = ''
147
-
tls {
148
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
149
-
}
150
-
151
-
reverse_proxy localhost:${toString cfg.port}
152
-
'';
153
-
};
154
-
155
-
# Register backup configuration
156
-
atelier.backup.services.emojibot = lib.mkIf cfg.backup.enable {
157
-
inherit (cfg.backup) paths exclude;
158
-
# Stateless service, no pre/post hooks needed
159
-
};
16
+
extraConfig = cfg: {
17
+
# Stateless - no data declarations needed
18
+
# Files are just the app code which is in git
160
19
};
161
20
}
+27
-138
modules/nixos/services/hn-alerts.nix
+27
-138
modules/nixos/services/hn-alerts.nix
···
1
-
{
2
-
config,
3
-
lib,
4
-
pkgs,
5
-
...
6
-
}:
7
-
let
8
-
cfg = config.atelier.services.hn-alerts;
9
-
in
10
-
{
11
-
options.atelier.services.hn-alerts = {
12
-
enable = lib.mkEnableOption "HN Alerts Hacker News monitoring service";
1
+
# HN Alerts - Hacker News monitoring service
2
+
#
3
+
# Has a database that needs backup
13
4
14
-
domain = lib.mkOption {
15
-
type = lib.types.str;
16
-
description = "Domain to serve hn-alerts on";
17
-
};
5
+
{ config, lib, pkgs, ... }:
18
6
19
-
port = lib.mkOption {
20
-
type = lib.types.port;
21
-
default = 3001;
22
-
description = "Port to run hn-alerts on";
23
-
};
7
+
let
8
+
mkService = import ../../lib/mkService.nix;
9
+
baseModule = mkService {
10
+
name = "hn-alerts";
11
+
description = "HN Alerts Hacker News monitoring service";
12
+
defaultPort = 3001;
13
+
runtime = "bun";
14
+
startCommand = "${pkgs.unstable.bun}/bin/bun start";
24
15
25
-
dataDir = lib.mkOption {
26
-
type = lib.types.path;
27
-
default = "/var/lib/hn-alerts";
28
-
description = "Directory to store hn-alerts data";
29
-
};
30
-
31
-
secretsFile = lib.mkOption {
32
-
type = lib.types.path;
33
-
description = "Path to secrets file containing SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_CHANNEL, SENTRY_DSN, DATABASE_URL";
34
-
};
35
-
36
-
repository = lib.mkOption {
37
-
type = lib.types.str;
38
-
default = "https://github.com/taciturnaxolotl/hn-alerts.git";
39
-
description = "Git repository URL (optional, for auto-deployment)";
40
-
};
41
-
42
-
autoUpdate = lib.mkEnableOption "Automatically git pull on service restart";
43
-
44
-
backup = {
45
-
enable = lib.mkEnableOption "Enable backups for hn-alerts" // { default = true; };
46
-
47
-
paths = lib.mkOption {
48
-
type = lib.types.listOf lib.types.str;
49
-
default = [ cfg.dataDir ];
50
-
description = "Paths to back up";
51
-
};
52
-
53
-
exclude = lib.mkOption {
54
-
type = lib.types.listOf lib.types.str;
55
-
default = [ "*.log" "app/.git" "app/node_modules" ];
56
-
description = "Patterns to exclude from backup";
16
+
extraConfig = cfg: {
17
+
# Data declarations for automatic backup
18
+
# App uses ./local.db relative to app dir by default
19
+
atelier.services.hn-alerts.data = {
20
+
sqlite = "${cfg.dataDir}/app/local.db";
57
21
};
58
22
};
59
23
};
24
+
cfg = config.atelier.services.hn-alerts;
25
+
in
26
+
{
27
+
imports = [ baseModule ];
60
28
29
+
# Add db:push to preStart (after the base preStart runs bun install)
61
30
config = lib.mkIf cfg.enable {
62
-
users.groups.services = { };
63
-
64
-
users.users.hn-alerts = {
65
-
isSystemUser = true;
66
-
group = "hn-alerts";
67
-
extraGroups = [ "services" ];
68
-
home = cfg.dataDir;
69
-
createHome = true;
70
-
shell = pkgs.bash;
71
-
};
72
-
73
-
users.groups.hn-alerts = { };
74
-
75
-
security.sudo.extraRules = [
76
-
{
77
-
users = [ "hn-alerts" ];
78
-
commands = [
79
-
{
80
-
command = "/run/current-system/sw/bin/systemctl restart hn-alerts.service";
81
-
options = [ "NOPASSWD" ];
82
-
}
83
-
];
84
-
}
85
-
];
86
-
87
-
systemd.services.hn-alerts = {
88
-
description = "HN Alerts Hacker News monitoring service";
89
-
wantedBy = [ "multi-user.target" ];
90
-
after = [ "network.target" ];
91
-
path = [ pkgs.git ];
92
-
93
-
preStart = ''
94
-
if [ ! -d ${cfg.dataDir}/app/.git ]; then
95
-
${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app
96
-
fi
97
-
98
-
cd ${cfg.dataDir}/app
99
-
'' + lib.optionalString cfg.autoUpdate ''
100
-
${pkgs.git}/bin/git pull
101
-
'' + ''
102
-
103
-
if [ ! -f src/index.ts ]; then
104
-
echo "No code found at ${cfg.dataDir}/app/src/index.ts"
105
-
exit 1
106
-
fi
107
-
108
-
echo "Installing dependencies..."
109
-
${pkgs.unstable.bun}/bin/bun install
110
-
111
-
echo "Initializing database..."
112
-
${pkgs.unstable.bun}/bin/bun run db:push
113
-
'';
114
-
115
-
serviceConfig = {
116
-
Type = "simple";
117
-
User = "hn-alerts";
118
-
Group = "hn-alerts";
119
-
EnvironmentFile = cfg.secretsFile;
120
-
Environment = [
121
-
"NODE_ENV=production"
122
-
"PORT=${toString cfg.port}"
123
-
];
124
-
ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${cfg.dataDir}/app && ${pkgs.unstable.bun}/bin/bun start'";
125
-
Restart = "always";
126
-
RestartSec = "10s";
127
-
};
128
-
};
129
-
130
-
services.caddy.virtualHosts.${cfg.domain} = {
131
-
extraConfig = ''
132
-
tls {
133
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
134
-
}
135
-
136
-
reverse_proxy localhost:${toString cfg.port}
137
-
'';
138
-
};
139
-
140
-
# Register backup configuration
141
-
atelier.backup.services.hn-alerts = lib.mkIf cfg.backup.enable {
142
-
inherit (cfg.backup) paths exclude;
143
-
# Has database, stop before backup
144
-
preBackup = "systemctl stop hn-alerts";
145
-
postBackup = "systemctl start hn-alerts";
146
-
};
31
+
systemd.services.hn-alerts.preStart = lib.mkAfter ''
32
+
echo "Initializing database..."
33
+
cd ${cfg.dataDir}/app
34
+
${pkgs.unstable.bun}/bin/bun run db:push || true
35
+
'';
147
36
};
148
37
}
+56
-174
modules/nixos/services/indiko.nix
+56
-174
modules/nixos/services/indiko.nix
···
1
-
{
2
-
config,
3
-
lib,
4
-
pkgs,
5
-
...
6
-
}:
1
+
# Indiko - IndieAuth/OAuth2 server
2
+
#
3
+
# Uses mkService base with custom rate limiting on auth endpoints
4
+
7
5
let
8
-
cfg = config.atelier.services.indiko;
6
+
mkService = import ../../lib/mkService.nix;
9
7
in
10
-
{
11
-
options.atelier.services.indiko = {
12
-
enable = lib.mkEnableOption "Indiko IndieAuth/OAuth2 server";
13
8
14
-
domain = lib.mkOption {
15
-
type = lib.types.str;
16
-
description = "Domain to serve Indiko on";
17
-
};
18
-
19
-
port = lib.mkOption {
20
-
type = lib.types.port;
21
-
default = 3003;
22
-
description = "Port to run Indiko on";
23
-
};
24
-
25
-
dataDir = lib.mkOption {
26
-
type = lib.types.path;
27
-
default = "/var/lib/indiko";
28
-
description = "Directory to store Indiko data";
29
-
};
9
+
mkService {
10
+
name = "indiko";
11
+
description = "Indiko IndieAuth/OAuth2 server";
12
+
defaultPort = 3003;
13
+
runtime = "bun";
14
+
entryPoint = "src/index.ts";
30
15
31
-
secretsFile = lib.mkOption {
32
-
type = lib.types.nullOr lib.types.path;
33
-
default = null;
34
-
description = ''
35
-
Path to secrets file (optional).
36
-
If you need additional environment variables, define them here.
37
-
'';
38
-
};
39
-
40
-
repository = lib.mkOption {
41
-
type = lib.types.str;
42
-
default = "https://github.com/taciturnaxolotl/indiko.git";
43
-
description = "Git repository URL (optional, for auto-deployment)";
44
-
};
45
-
46
-
autoUpdate = lib.mkEnableOption "Automatically git pull on service restart";
47
-
48
-
backup = {
49
-
enable = lib.mkEnableOption "Enable backups for indiko" // { default = true; };
50
-
51
-
paths = lib.mkOption {
52
-
type = lib.types.listOf lib.types.str;
53
-
default = [ cfg.dataDir ];
54
-
description = "Paths to back up";
55
-
};
56
-
57
-
exclude = lib.mkOption {
58
-
type = lib.types.listOf lib.types.str;
59
-
default = [ "*.log" "app/.git" "app/node_modules" ];
60
-
description = "Patterns to exclude from backup";
61
-
};
62
-
};
63
-
};
64
-
65
-
config = lib.mkIf cfg.enable {
66
-
users.groups.services = { };
67
-
68
-
users.users.indiko = {
69
-
isSystemUser = true;
70
-
group = "indiko";
71
-
extraGroups = [ "services" ];
72
-
home = cfg.dataDir;
73
-
createHome = true;
74
-
shell = pkgs.bash;
75
-
};
76
-
77
-
users.groups.indiko = { };
78
-
79
-
security.sudo.extraRules = [
80
-
{
81
-
users = [ "indiko" ];
82
-
commands = [
83
-
{
84
-
command = "/run/current-system/sw/bin/systemctl restart indiko.service";
85
-
options = [ "NOPASSWD" ];
86
-
}
87
-
];
88
-
}
16
+
extraConfig = cfg: {
17
+
# Add ORIGIN and RP_ID environment variables
18
+
systemd.services.indiko.serviceConfig.Environment = [
19
+
"ORIGIN=https://${cfg.domain}"
20
+
"RP_ID=${cfg.domain}"
89
21
];
90
22
91
-
systemd.services.indiko = {
92
-
description = "Indiko IndieAuth/OAuth2 server";
93
-
wantedBy = [ "multi-user.target" ];
94
-
after = [ "network.target" ];
95
-
path = [ pkgs.git ];
96
-
97
-
preStart = ''
98
-
if [ ! -d ${cfg.dataDir}/app/.git ]; then
99
-
${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app
100
-
fi
101
-
102
-
cd ${cfg.dataDir}/app
103
-
'' + lib.optionalString cfg.autoUpdate ''
104
-
${pkgs.git}/bin/git pull
105
-
'' + ''
106
-
107
-
if [ ! -f src/index.ts ]; then
108
-
echo "No code found at ${cfg.dataDir}/app/src/index.ts"
109
-
exit 1
110
-
fi
111
-
112
-
echo "Installing dependencies..."
113
-
${pkgs.unstable.bun}/bin/bun install
114
-
'';
115
-
116
-
serviceConfig = {
117
-
Type = "simple";
118
-
User = "indiko";
119
-
Group = "indiko";
120
-
EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile;
121
-
Environment = [
122
-
"NODE_ENV=production"
123
-
"PORT=${toString cfg.port}"
124
-
"ORIGIN=https://${cfg.domain}"
125
-
"RP_ID=${cfg.domain}"
126
-
];
127
-
ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${cfg.dataDir}/app && ${pkgs.unstable.bun}/bin/bun run src/index.ts'";
128
-
Restart = "always";
129
-
RestartSec = "10s";
130
-
};
131
-
132
-
serviceConfig.ExecStartPre = [
133
-
"+${pkgs.writeShellScript "indiko-setup" ''
134
-
mkdir -p ${cfg.dataDir}/app
135
-
chown -R indiko:services ${cfg.dataDir}
136
-
chmod -R g+rwX ${cfg.dataDir}
137
-
''}"
138
-
];
139
-
};
23
+
# Custom Caddy config with rate limiting on auth endpoints
24
+
services.caddy.virtualHosts.${cfg.domain}.extraConfig = ''
25
+
tls {
26
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
27
+
}
140
28
141
-
services.caddy.virtualHosts.${cfg.domain} = {
142
-
extraConfig = ''
143
-
tls {
144
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
29
+
# Rate limiting for auth endpoints
30
+
handle /auth/* {
31
+
rate_limit {
32
+
zone auth_limit {
33
+
key {http.request.remote_ip}
34
+
events 10
35
+
window 1m
36
+
}
145
37
}
38
+
reverse_proxy localhost:${toString cfg.port}
39
+
}
146
40
147
-
# Rate limiting for auth endpoints
148
-
handle /auth/* {
149
-
rate_limit {
150
-
zone auth_limit {
151
-
key {http.request.remote_ip}
152
-
events 10
153
-
window 1m
154
-
}
41
+
# Rate limiting for API endpoints
42
+
handle /api/* {
43
+
rate_limit {
44
+
zone api_limit {
45
+
key {http.request.remote_ip}
46
+
events 30
47
+
window 1m
155
48
}
156
-
reverse_proxy localhost:${toString cfg.port}
157
49
}
50
+
reverse_proxy localhost:${toString cfg.port}
51
+
}
158
52
159
-
# Rate limiting for API endpoints
160
-
handle /api/* {
161
-
rate_limit {
162
-
zone api_limit {
163
-
key {http.request.remote_ip}
164
-
events 30
165
-
window 1m
166
-
}
53
+
# General rate limiting for all other routes
54
+
handle {
55
+
rate_limit {
56
+
zone general_limit {
57
+
key {http.request.remote_ip}
58
+
events 60
59
+
window 1m
167
60
}
168
-
reverse_proxy localhost:${toString cfg.port}
169
61
}
62
+
reverse_proxy localhost:${toString cfg.port}
63
+
}
64
+
'';
170
65
171
-
# General rate limiting for all other routes
172
-
handle {
173
-
rate_limit {
174
-
zone general_limit {
175
-
key {http.request.remote_ip}
176
-
events 60
177
-
window 1m
178
-
}
179
-
}
180
-
reverse_proxy localhost:${toString cfg.port}
181
-
}
182
-
'';
183
-
};
66
+
# Disable default caddy config since we're overriding it
67
+
atelier.services.indiko.caddy.enable = false;
184
68
185
-
# Register backup configuration
186
-
atelier.backup.services.indiko = lib.mkIf cfg.backup.enable {
187
-
inherit (cfg.backup) paths exclude;
188
-
# Has SQLite database for sessions/tokens
189
-
preBackup = "systemctl stop indiko";
190
-
postBackup = "systemctl start indiko";
69
+
# Data declarations for automatic backup (SQLite for sessions/tokens)
70
+
# App uses hardcoded data/indiko.db relative to app dir
71
+
atelier.services.indiko.data = {
72
+
sqlite = "${cfg.dataDir}/app/data/indiko.db";
191
73
};
192
74
};
193
75
}
+28
-152
modules/nixos/services/l4.nix
+28
-152
modules/nixos/services/l4.nix
···
1
-
{
2
-
config,
3
-
lib,
4
-
pkgs,
5
-
...
6
-
}:
7
-
let
8
-
cfg = config.atelier.services.l4;
9
-
in
10
-
{
11
-
options.atelier.services.l4 = {
12
-
enable = lib.mkEnableOption "L4 Image CDN service";
13
-
14
-
domain = lib.mkOption {
15
-
type = lib.types.str;
16
-
description = "Domain to serve L4 on";
17
-
};
18
-
19
-
port = lib.mkOption {
20
-
type = lib.types.port;
21
-
default = 3004;
22
-
description = "Port to run L4 on";
23
-
};
24
-
25
-
dataDir = lib.mkOption {
26
-
type = lib.types.path;
27
-
default = "/var/lib/l4";
28
-
description = "Directory to store L4 data";
29
-
};
30
-
31
-
secretsFile = lib.mkOption {
32
-
type = lib.types.path;
33
-
description = ''
34
-
Path to secrets file containing:
35
-
- R2_ACCOUNT_ID
36
-
- R2_ACCESS_KEY_ID
37
-
- R2_SECRET_ACCESS_KEY
38
-
- R2_BUCKET
39
-
- R2_PUBLIC_URL
40
-
- SLACK_BOT_TOKEN
41
-
- SLACK_SIGNING_SECRET
42
-
- ALLOWED_CHANNELS (optional, comma-separated channel IDs)
43
-
'';
44
-
};
45
-
46
-
repository = lib.mkOption {
47
-
type = lib.types.str;
48
-
default = "https://github.com/taciturnaxolotl/l4.git";
49
-
description = "Git repository URL (optional, for auto-deployment)";
50
-
};
1
+
# L4 - Image CDN / Slack image optimizer
2
+
#
3
+
# Images stored in R2, but keeps local stats
51
4
52
-
autoUpdate = lib.mkEnableOption "Automatically git pull on service restart";
5
+
{ config, lib, pkgs, ... }:
53
6
54
-
backup = {
55
-
enable = lib.mkEnableOption "Enable backups for l4" // { default = true; };
7
+
let
8
+
mkService = import ../../lib/mkService.nix;
9
+
baseModule = mkService {
10
+
name = "l4";
11
+
description = "L4 Image CDN - Slack image optimizer and R2 uploader";
12
+
defaultPort = 3004;
13
+
runtime = "bun";
14
+
entryPoint = "src/index.ts";
56
15
57
-
paths = lib.mkOption {
58
-
type = lib.types.listOf lib.types.str;
59
-
default = [ cfg.dataDir ];
60
-
description = "Paths to back up";
16
+
extraConfig = cfg: {
17
+
# Add PUBLIC_URL environment variable
18
+
atelier.services.l4.environment = {
19
+
PUBLIC_URL = "https://${cfg.domain}";
61
20
};
62
21
63
-
exclude = lib.mkOption {
64
-
type = lib.types.listOf lib.types.str;
65
-
default = [ "*.log" "app/.git" "app/node_modules" ];
66
-
description = "Patterns to exclude from backup";
22
+
# Data declarations for backup (SQLite stats database)
23
+
# App runs from ${dataDir}/app and uses ./data/stats.db
24
+
atelier.services.l4.data = {
25
+
sqlite = "${cfg.dataDir}/app/data/stats.db";
67
26
};
68
27
};
69
28
};
70
-
29
+
cfg = config.atelier.services.l4;
30
+
in
31
+
{
32
+
imports = [ baseModule ];
33
+
34
+
# Add LD_LIBRARY_PATH for native dependencies (sharp image processing)
71
35
config = lib.mkIf cfg.enable {
72
-
users.groups.services = { };
73
-
74
-
users.users.l4 = {
75
-
isSystemUser = true;
76
-
group = "l4";
77
-
extraGroups = [ "services" ];
78
-
home = cfg.dataDir;
79
-
createHome = true;
80
-
shell = pkgs.bash;
81
-
};
82
-
83
-
users.groups.l4 = { };
84
-
85
-
security.sudo.extraRules = [
86
-
{
87
-
users = [ "l4" ];
88
-
commands = [
89
-
{
90
-
command = "/run/current-system/sw/bin/systemctl restart l4.service";
91
-
options = [ "NOPASSWD" ];
92
-
}
93
-
];
94
-
}
95
-
];
96
-
97
-
systemd.services.l4 = {
98
-
description = "L4 Image CDN - Slack image optimizer and R2 uploader";
99
-
wantedBy = [ "multi-user.target" ];
100
-
after = [ "network.target" ];
101
-
path = [ pkgs.git pkgs.stdenv.cc.cc.lib ];
102
-
103
-
preStart = ''
104
-
if [ ! -d ${cfg.dataDir}/app/.git ]; then
105
-
${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app
106
-
fi
107
-
108
-
cd ${cfg.dataDir}/app
109
-
'' + lib.optionalString cfg.autoUpdate ''
110
-
${pkgs.git}/bin/git pull
111
-
'' + ''
112
-
113
-
if [ ! -f src/index.ts ]; then
114
-
echo "No code found at ${cfg.dataDir}/app/src/index.ts"
115
-
exit 1
116
-
fi
117
-
118
-
echo "Installing dependencies..."
119
-
${pkgs.unstable.bun}/bin/bun install
120
-
'';
121
-
122
-
serviceConfig = {
123
-
Type = "simple";
124
-
User = "l4";
125
-
Group = "l4";
126
-
EnvironmentFile = cfg.secretsFile;
127
-
Environment = [
128
-
"NODE_ENV=production"
129
-
"PORT=${toString cfg.port}"
130
-
"PUBLIC_URL=https://${cfg.domain}"
131
-
"LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib"
132
-
];
133
-
ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${cfg.dataDir}/app && ${pkgs.unstable.bun}/bin/bun run src/index.ts'";
134
-
Restart = "always";
135
-
RestartSec = "10s";
136
-
};
137
-
138
-
serviceConfig.ExecStartPre = [
139
-
"+${pkgs.writeShellScript "l4-setup" ''
140
-
mkdir -p ${cfg.dataDir}/app
141
-
chown -R l4:services ${cfg.dataDir}
142
-
chmod -R g+rwX ${cfg.dataDir}
143
-
''}"
144
-
];
145
-
};
146
-
147
-
services.caddy.virtualHosts.${cfg.domain} = {
148
-
extraConfig = ''
149
-
tls {
150
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
151
-
}
152
-
153
-
reverse_proxy localhost:${toString cfg.port}
154
-
'';
155
-
};
156
-
157
-
# Register backup configuration
158
-
atelier.backup.services.l4 = lib.mkIf cfg.backup.enable {
159
-
inherit (cfg.backup) paths exclude;
160
-
# Stateless service (images in R2), no pre/post hooks needed
161
-
};
36
+
systemd.services.l4.environment.LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib";
37
+
systemd.services.l4.path = [ pkgs.stdenv.cc.cc.lib ];
162
38
};
163
39
}
+6
-6
modules/nixos/services/restic/README.md
+6
-6
modules/nixos/services/restic/README.md
···
101
101
The `atelier-backup` command provides an interactive TUI:
102
102
103
103
```bash
104
-
atelier-backup # Interactive menu
105
-
atelier-backup status # Show backup status for all services
106
-
atelier-backup list # Browse snapshots
107
-
atelier-backup backup # Trigger manual backup
108
-
atelier-backup restore # Interactive restore wizard
109
-
atelier-backup dr # Disaster recovery mode
104
+
sudo atelier-backup # Interactive menu
105
+
sudo atelier-backup status # Show backup status for all services
106
+
sudo atelier-backup list # Browse snapshots
107
+
sudo atelier-backup backup # Trigger manual backup
108
+
sudo atelier-backup restore # Interactive restore wizard
109
+
sudo atelier-backup dr # Disaster recovery mode
110
110
```
111
111
112
112
See `man atelier-backup` for full documentation.
+12
-12
modules/nixos/services/restic/atelier-backup.1.md
+12
-12
modules/nixos/services/restic/atelier-backup.1.md
···
8
8
9
9
# SYNOPSIS
10
10
11
-
**atelier-backup** [*COMMAND*]
11
+
**sudo atelier-backup** [*COMMAND*]
12
12
13
-
**atelier-backup** **status**
13
+
**sudo atelier-backup** **status**
14
14
15
-
**atelier-backup** **list**
15
+
**sudo atelier-backup** **list**
16
16
17
-
**atelier-backup** **backup**
17
+
**sudo atelier-backup** **backup**
18
18
19
-
**atelier-backup** **restore**
19
+
**sudo atelier-backup** **restore**
20
20
21
-
**atelier-backup** **dr**
21
+
**sudo atelier-backup** **dr**
22
22
23
23
# DESCRIPTION
24
24
···
73
73
74
74
Interactive menu:
75
75
```
76
-
$ atelier-backup
76
+
$ sudo atelier-backup
77
77
```
78
78
79
79
Check backup status for all services:
80
80
```
81
-
$ atelier-backup status
81
+
$ sudo atelier-backup status
82
82
```
83
83
84
84
Browse snapshots for a service:
85
85
```
86
-
$ atelier-backup list
86
+
$ sudo atelier-backup list
87
87
```
88
88
89
89
Trigger manual backup:
90
90
```
91
-
$ atelier-backup backup
91
+
$ sudo atelier-backup backup
92
92
```
93
93
94
94
Restore a service from backup:
95
95
```
96
-
$ atelier-backup restore
96
+
$ sudo atelier-backup restore
97
97
```
98
98
99
99
Full disaster recovery:
100
100
```
101
-
$ atelier-backup dr
101
+
$ sudo atelier-backup dr
102
102
```
103
103
104
104
# FILES
+12
-2
modules/nixos/services/restic/cli.nix
+12
-2
modules/nixos/services/restic/cli.nix
···
57
57
input() { ${pkgs.gum}/bin/gum input "$@"; }
58
58
spin() { ${pkgs.gum}/bin/gum spin "$@"; }
59
59
60
+
# Check for root
61
+
if [ "$(id -u)" -ne 0 ]; then
62
+
style --foreground 196 "Error: atelier-backup must be run as root"
63
+
echo "Try: sudo atelier-backup $*"
64
+
exit 1
65
+
fi
66
+
60
67
# Restic wrapper with secrets
61
68
restic_cmd() {
62
69
${pkgs.restic}/bin/restic \
···
65
72
"$@"
66
73
}
67
74
export -f restic_cmd
68
-
export B2_ACCOUNT_ID=$(cat ${config.age.secrets."restic/env".path} | grep B2_ACCOUNT_ID | cut -d= -f2)
69
-
export B2_ACCOUNT_KEY=$(cat ${config.age.secrets."restic/env".path} | grep B2_ACCOUNT_KEY | cut -d= -f2)
75
+
76
+
# Load B2 credentials from environment file
77
+
set -a
78
+
source ${config.age.secrets."restic/env".path}
79
+
set +a
70
80
71
81
# Available services
72
82
SERVICES="${lib.concatStringsSep " " allBackupServices}"