Kieran's opinionated (and probably slightly dumb) nix config

feat: add rest of the services to backup and remove direnv

dunkirk.sh 735743c4 1f8b22d4

verified
-1
.envrc
··· 1 - eval "$(nix print-dev-env)"
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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}"