# ============================================================================== # PROJECT SKELETON — NixOS Service Module # ============================================================================== # # USAGE: # 1. Copy this file into your project repo, e.g. ~/projects/my-project/my-project.nix # 2. Replace every occurrence of: # MY_PROJECT → your project name in camelCase (e.g. myRssReader) # my-project → your project name in kebab-case (e.g. my-rss-reader) # my-project.example.com → your actual domain # 127.0.0.1:3000 → the address your app listens on internally # 3. In /etc/nixos/configuration.nix, add: # imports = [ /path/to/my-project.nix ]; # services.MY_PROJECT.enable = true; # # ASSUMPTIONS: # - Nginx global settings (recommendedTlsSettings etc.) and ACME email are # configured centrally in configuration.nix (see bottom of this file). # - Your app runs as a systemd service on a local port (reverse proxy setup). # - You want HTTPS via Let's Encrypt. # # ============================================================================== { config, pkgs, lib, ... }: let # The internal port your application listens on. # Nginx will forward public traffic here. appPort = 3000; # Shorthand so you can reference the merged config of this module cleanly. cfg = config.services.MY_PROJECT; in { # ============================================================================ # OPTIONS # Define the knobs that other files (or configuration.nix) can turn. # Nothing in `config` below runs until `enable` is set to true. # ============================================================================ options.services.MY_PROJECT = { enable = lib.mkEnableOption "MY_PROJECT service"; # Adds a boolean option `services.MY_PROJECT.enable` that defaults to false. # Set it to true in configuration.nix to activate this module. domain = lib.mkOption { type = lib.types.str; default = "my-project.example.com"; description = "The public domain name Nginx will serve and ACME will certify."; }; port = lib.mkOption { type = lib.types.port; default = appPort; description = "Internal port the application process listens on."; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/my-project"; description = "Directory for persistent application data (database, uploads, etc.)."; }; # Optional: expose an environment file path for secrets. # The file should contain KEY=VALUE pairs and never be committed to git. # Example content: # DATABASE_URL=postgres://user:password@localhost/mydb # SECRET_KEY=supersecret environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Path to a file containing secret environment variables. This file is read by systemd at runtime and never stored in the Nix store. Example: /run/secrets/my-project.env ''; }; }; # ============================================================================ # CONFIG # Everything below only takes effect when `services.MY_PROJECT.enable = true`. # ============================================================================ config = lib.mkIf cfg.enable { # -------------------------------------------------------------------------- # Systemd Service # Runs your application as a background process. # -------------------------------------------------------------------------- systemd.services.my-project = { description = "MY_PROJECT application service"; # Start after the network and (if used) PostgreSQL are ready. after = [ "network.target" "postgresql.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { # --- Replace this with your actual start command --- # Examples: # Node.js: "${pkgs.nodejs}/bin/node ${cfg.dataDir}/server.js" # Python: "${pkgs.python3}/bin/python ${cfg.dataDir}/app.py" # Binary: "${pkgs.my-package}/bin/my-binary" ExecStart = "${pkgs.nodejs}/bin/node ${cfg.dataDir}/server.js"; # Working directory for the process. WorkingDirectory = cfg.dataDir; # DynamicUser: systemd creates an unprivileged user automatically. # The user has no fixed UID and cannot log in. Good for most services. # Set to false if you need a persistent user (e.g. for file ownership). DynamicUser = true; # Give the dynamic user write access to the data directory. StateDirectory = "my-project"; StateDirectoryMode = "0750"; # Load secrets from a file at runtime (optional). # The contents are injected as environment variables. EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; # Hardening — remove lines you don't need, but keep as many as possible. NoNewPrivileges = true; PrivateTmp = true; # /tmp is isolated from other services ProtectSystem = "strict"; ProtectHome = true; RestrictSUIDSGID = true; LockPersonality = true; RestrictNamespaces = true; # Restart automatically if the process crashes. Restart = "on-failure"; RestartSec = "5s"; }; # Environment variables that are safe to be in the Nix store. # For secrets, use environmentFile above instead. environment = { NODE_ENV = "production"; PORT = toString cfg.port; DATA_DIR = cfg.dataDir; }; }; # -------------------------------------------------------------------------- # Nginx Virtual Host # Terminates TLS and proxies requests to the local application port. # -------------------------------------------------------------------------- services.nginx = { # Nginx is enabled automatically when virtualHosts is non-empty, # but being explicit is clearer. enable = true; virtualHosts.${cfg.domain} = { # Redirect all plain HTTP to HTTPS. forceSSL = true; # Let NixOS manage the Let's Encrypt certificate automatically. # Renewal is handled by a systemd timer — no manual cron needed. # Requires: security.acme.acceptTerms = true and defaults.email set # in your central configuration.nix. enableACME = true; # --- Main reverse proxy location --- locations."/" = { proxyPass = "http://127.0.0.1:${toString cfg.port}"; # Needed for apps that use WebSockets (e.g. live reload, chat). # Remove these two lines if your app does not use WebSockets. proxyWebsockets = true; extraConfig = '' proxy_read_timeout 60s; ''; }; # --- Optional: serve static files directly from Nginx --- # Uncomment and adjust if your app has a public assets directory. # Serving static files from Nginx is faster than going through Node/Python. # # locations."/static/" = { # alias = "${cfg.dataDir}/static/"; # extraConfig = '' # expires 30d; # add_header Cache-Control "public, immutable"; # ''; # }; # --- Optional: block access to sensitive paths --- # locations."~ /\\." = { # extraConfig = "deny all;"; # }; }; }; # -------------------------------------------------------------------------- # PostgreSQL Database (optional) # Uncomment this block if your project needs a database. # -------------------------------------------------------------------------- # services.postgresql = { # enable = true; # # # Creates the database and user if they don't exist yet. # ensureDatabases = [ "my-project" ]; # ensureUsers = [{ # name = "my-project"; # ensureDBOwnership = true; # }]; # }; # -------------------------------------------------------------------------- # Firewall # Ports 80 and 443 must be open for Nginx and Let's Encrypt to work. # If these are already opened in configuration.nix, remove this block. # -------------------------------------------------------------------------- networking.firewall.allowedTCPPorts = [ 80 443 ]; }; # ============================================================================ # CENTRAL CONFIGURATION.NIX REQUIREMENTS # # Make sure these are set somewhere in your configuration.nix (not here, # so they are shared across all project modules without duplication): # # services.nginx = { # recommendedGzipSettings = true; # recommendedOptimisation = true; # recommendedTlsSettings = true; # recommendedProxySettings = true; # }; # # security.acme = { # acceptTerms = true; # defaults.email = "your@email.com"; # }; # # ============================================================================ }