I am deploying a dynamic backend web service to my NixOS server.
Is there any way I can get a quicker feedback loop for deploying this web service, and retain it as part of the system configuration? I really like that it gets included when redeploying the server. But I don’t like that nix flake update updates both the service input and nixpkgs (since I run unstable). Deploying tomorrow is going to take longer for no good reason, and deploying today, even, is also going to take longer than necessary.
I’m running a systemd service behind an nginx reverse proxy.
I could perhaps look more into sandboxing the app.
Maybe there’s a way to “hotswap” the app so that a redeployment of the server will still upgrade the service, but hotswapping will do so around the system configuration? Right now my app is in a package (derivation), which makes this a little difficult, I think. Could I move the app’s assets to a mutable place on disk outside the store? E.g. on a ramdisk. I’d need to restart the systemd service as part of deploying.
Perhaps I’m ignorant of any pre-made solutions out there.
# service.nix
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.birthday-rsvp;
mkProxySite = domain: port: {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString port}";
proxyWebsockets = true;
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
'';
};
extraConfig = ''
access_log /var/log/nginx/${domain}.access.log;
error_log /var/log/nginx/${domain}.error.log;
'';
};
in
{
options.services.birthday-rsvp = {
enable = mkEnableOption "Birthday RSVP Flask application";
package = mkOption {
type = types.package;
default = pkgs.callPackage ./app.nix { };
defaultText = literalExpression "pkgs.callPackage ./app.nix { }";
description = "The birthday-rsvp package to use.";
};
domain = mkOption {
type = types.str;
example = "birthday.example.com";
description = "Domain name for the nginx virtual host.";
};
port = mkOption {
type = types.port;
default = 5000;
description = "Port on which the Flask application listens.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/birthday-rsvp";
description = "Directory to store the SQLite database and application data.";
};
user = mkOption {
type = types.str;
default = "birthday-rsvp";
description = "User under which the birthday-rsvp service runs.";
};
group = mkOption {
type = types.str;
default = "birthday-rsvp";
description = "Group under which the birthday-rsvp service runs.";
};
secretKey = mkOption {
type = types.str;
default = "change-me-in-production";
description = "Flask secret key for session management.";
};
baseUrl = mkOption {
type = types.str;
default = "https://${cfg.domain}";
defaultText = literalExpression ''"https://$${cfg.domain}"'';
description = "Base URL for generating invite links in the overview.";
};
};
config = mkIf cfg.enable {
# Create user and group
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
createHome = true;
description = "Birthday RSVP service user";
};
users.groups.${cfg.group} = {};
# Systemd service
systemd.services.birthday-rsvp = {
description = "Birthday RSVP Flask Application";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
EVENTS_DB_PATH = "${cfg.dataDir}/events.db";
FLASK_ENV = "production";
FLASK_SECRET_KEY = cfg.secretKey;
RSVP_BASE_URL = cfg.baseUrl;
};
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.dataDir;
ExecStart = "${cfg.package}/bin/birthday-rsvp-app";
Restart = "always";
RestartSec = 10;
# Security settings
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir ];
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
};
preStart = ''
# Ensure data directory exists and has correct permissions
mkdir -p ${cfg.dataDir}
chown ${cfg.user}:${cfg.group} ${cfg.dataDir}
# Initialize database if it doesn't exist
if [ ! -f ${cfg.dataDir}/events.db ]; then
cd ${cfg.dataDir}
${cfg.package}/bin/birthday-rsvp-init
chown ${cfg.user}:${cfg.group} ${cfg.dataDir}/events.db
fi
'';
};
# Nginx configuration
services.nginx = {
enable = true;
virtualHosts.${cfg.domain} = mkProxySite cfg.domain cfg.port;
};
# Open firewall for HTTP and HTTPS
networking.firewall.allowedTCPPorts = [ 80 443 ];
};
}