Package Next.js App as Nix Derivation and deploy as Service on NixOS

With this method you can package any type of Next.js App including Apps w/ SSR, API Routes, Server Actions and Proxy (Middleware).

Prerequisites

  • Obviously Git
  • Your Next.js App remote Repository w/ package.json and package-lock.json
  • Your NixOS Server

Getting started

  • Add the required output mode to your next.config.ts:
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
    output: "standalone",
};

export default nextConfig;
  • Your npmDepsHash is the unique id of your package-lock.json. We will need it for the next step, so prefetch it:
nix run --extra-experimental-features "nix-command flakes" nixpkgs#prefetch-npm-deps package-lock.json
  • Create your own flake.nix in the same directory as the package.json, make sure to edit the generic “Your Next.js App” Labels and insert your prefetched npmDepsHash from the last step:
{
  description = "Your Next.js App Description";

  # Feel free to choose the input channel yourself 
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      # Support all Nix-capable Systems
      supportedSystems = [
        "x86_64-linux"   # Standard Intel/AMD Linux
        "aarch64-linux"  # ARM Linux
        "x86_64-darwin"  # Intel Macs
        "aarch64-darwin" # Apple Silicon Macs
      ];

      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
    in {
      packages = forAllSystems (system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in {
          default = pkgs.buildNpmPackage {
            pname = "your-nextjs-app";
            version = "1.0.0";
            src = ./.;

            # Insert your prefetched npmDepsHash from the last step
            npmDepsHash = "sha256-...";

            postBuild = ''
              cp -r public .next/standalone/
              cp -r .next/static .next/standalone/.next/
            '';

            installPhase = ''
              cp -r .next/standalone $out
            '';
          };
        }
      );

      nixosModules.default = { config, lib, pkgs, ... }:
        let
          cfg = config.services.your-nextjs-app;

          basePackage = self.packages.${pkgs.system}.default;

          customPackage = basePackage.overrideAttrs (oldAttrs: cfg.environment);
        in {
          options.services.your-nextjs-app = {
            enable = lib.mkEnableOption "Your Next.js App";

            environment = lib.mkOption {
              type = lib.types.attrsOf lib.types.str;
              default = {};
              description = "Key-Value Pairs for Build-Time and Run-Time";
              example = {
                PORT = "3000";
                NODE_ENV = "production";
              };
            };
          };

          config = lib.mkIf cfg.enable {
            systemd.services.your-nextjs-app = {
              description = "Your Next.js App Service";
              wantedBy = [ "multi-user.target" ];
              after = [ "network.target" ];

              environment = cfg.environment;

              serviceConfig = {
                ExecStart = "${pkgs.nodejs_24}/bin/node ${customPackage}/server.js";
                Restart = "always";
              };
            };
          };
        };
    };
}

With every package update and newly added package to your Next.js App you will need to prefetch your npmDepsHash again (repeat step 2) and update your flake.nix.

  • Generate your flake.lock for reproducitbility, commit and push:
nix flake lock --extra-experimental-features "nix-command flakes" && git commit -m "Added flake.nix and flake.lock" && git push
  • Update your NixOS Server. Example minimal NixOS Build Flake, keep in mind to replace all generic values:
{
  description = "Your NixOS Server";

  # Feel free to choose the input channel yourself
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    your-nextjs-app.url = "github:YourUsername/YourRepository"; 
    # Example input with Codeberg SSH: 
    # "git+ssh://[email protected]/YourUsername/YourRepository.git"
  };

  outputs = { self, nixpkgs, your-nextjs-app, ... }:
    {
      nixosConfigurations = {
        "your-hostname-x86_64" = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ configuration.nix ];
        };

        "your-hostname-aarch64" = nixpkgs.lib.nixosSystem {
          system = "aarch64-linux";
          modules = [ configuration.nix ];
        };
      };
    };
}
  • Use the provided Service of your Package. Example minimal configuration.nix in the same directory as flake.nix from step 5, keep in mind to replace all generic values:
{ config, pkgs, lib, ... }:
{
  # Demo User feel free to insert your own
  users.users."your-user" = {
      isNormalUser = true;
      initialPassword = "changeme";
      extraGroups = [ "wheel" ];
      openssh.authorizedKeys.keys = "Your .pub SSH Key";
    };

  services.your-nextjs-app = {
    enable = true;

    # Define all environment variables your App needs
    environment = {
      PORT = "3000";
      NODE_ENV = "production";
    };
  };

  networking = {
    hostname = "your-hostname";

    firewall = {
      enable = true;

      # Open the Port used by your Next.js App
      allowedTCPPorts = [ 3000 ];
    };
}
  • Rebuild your NixOS System, keep in mind to replace all generic values:
nixos-rebuild switch --flake /path/to/flakeAndConf#<your-hostname-x86_64> or <your-hostname-aarch64> --extra-experimental-features "nix-command flakes"
  • Now the your Next.js App should be available as a Service on your NixOS Server. Enjoy <3