Examples

Worked, copy-paste-ready configs you can build on. Each example is a complete flake or module with a short explanation and the exact command to apply it. Start at the top if you are new, or jump to the one that matches what you are building. The attribute names match what Ternix generates, so a host is nixosConfigurations.nixos-host, a user is homeConfigurations.user, and the files are flake.nix, configuration.nix, and home.nix.

Everything here follows the official NixOS manual and nixpkgs . Where an option might be unfamiliar, the surrounding text says what it does and why it is there.

A minimal NixOS flake

The smallest config that still builds a real system is two files. A flake.nix that declares one input and one output, and a configuration.nix that describes the machine.

# flake.nix
{
  description = "Minimal NixOS host";
 
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 
  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations.nixos-host = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}
# configuration.nix
{ pkgs, ... }:
{
  imports = [ ./hardware-configuration.nix ];
 
  boot.loader.systemd-boot.enable = true;
 
  networking.hostName = "nixos-host";
 
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
 
  users.users.user = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
  };
 
  environment.systemPackages = [ pkgs.vim pkgs.git ];
 
  system.stateVersion = "24.11";
}

The flake exposes a single host named nixos-host. The module list points at configuration.nix, which in turn imports the hardware scan that nixos-generate-config wrote for your disks. system.stateVersion pins the release whose defaults this machine expects, so leave it at the value you first installed with. Build and activate it with the host name after the #.

sudo nixos-rebuild switch --flake .#nixos-host

A real application, a web server host

This is a full host you could put on the public internet. It serves a website over HTTPS with a certificate from Let's Encrypt, and it runs a small custom application of your own behind nginx. Read it block by block, because every part maps to something a deployed server needs.

# configuration.nix
{ pkgs, ... }:
 
let
  # A tiny app built straight from a shell script. It writes an index page once
  # and then serves the current directory on a loopback port. Swap the body for
  # your real Node or Python service when you have one.
  helloWeb = pkgs.writeShellApplication {
    name = "hello-web";
    runtimeInputs = [ pkgs.python3 ];
    text = ''
      cat > index.html <<'HTML'
      <!doctype html>
      <title>Served from NixOS</title>
      <h1>It works.</h1>
      HTML
      exec python3 -m http.server 8080 --bind 127.0.0.1
    '';
  };
in
{
  imports = [ ./hardware-configuration.nix ];
 
  boot.loader.systemd-boot.enable = true;
  networking.hostName = "nixos-host";
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  system.stateVersion = "24.11";
 
  # Make the app available on the PATH as well as to the service below.
  environment.systemPackages = [ helloWeb ];
 
  # Run the custom app as a managed service. DynamicUser gives it a throwaway
  # unprivileged account, StateDirectory creates /var/lib/hello-web for it, and
  # Restart keeps it alive across crashes.
  systemd.services.hello-web = {
    description = "Custom hello web app";
    wantedBy = [ "multi-user.target" ];
    after = [ "network.target" ];
    serviceConfig = {
      ExecStart = "${helloWeb}/bin/hello-web";
      DynamicUser = true;
      StateDirectory = "hello-web";
      WorkingDirectory = "/var/lib/hello-web";
      Restart = "on-failure";
    };
  };
 
  # The public-facing web server. It terminates TLS and forwards requests to the
  # app on 127.0.0.1:8080, so the app itself never has to face the internet.
  services.nginx = {
    enable = true;
    recommendedProxySettings = true;
    recommendedTlsSettings = true;
 
    virtualHosts."example.com" = {
      forceSSL = true;
      enableACME = true;
      locations."/".proxyPass = "http://127.0.0.1:8080";
    };
  };
 
  # ACME fetches and renews the Let's Encrypt certificate for the host above.
  # You must accept the CA terms and give an email for expiry notices.
  security.acme = {
    acceptTerms = true;
    defaults.email = "admin@example.com";
  };
 
  # Only 80 and 443 are open. Port 80 is needed for the ACME HTTP challenge and
  # for redirecting plain HTTP to HTTPS, 443 serves the real traffic.
  networking.firewall.allowedTCPPorts = [ 80 443 ];
 
  users.users.user = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
  };
}

A few things make this safe rather than just functional. The app binds to 127.0.0.1, so the only way in from outside is through nginx, which gives you TLS, logging, and a single front door. forceSSL plus enableACME means nginx redirects HTTP to HTTPS and wires the certificate in automatically, and recommendedTlsSettings applies a sane cipher and protocol baseline so you are not hand-tuning OpenSSL. The firewall stays closed except for the two ports the setup actually uses.

Three real-world notes before you deploy. The domain in virtualHosts has to resolve to this machine's public IP, otherwise the ACME challenge fails and no certificate is issued. Swap admin@example.com for an address you actually read, since Let's Encrypt sends expiry warnings there. While you are testing, point security.acme.defaults at the Let's Encrypt staging environment so you do not hit the rate limit on the production CA.

security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";

Apply it the same way as any host. After it switches, the service is live and the certificate is requested on the first request to the domain.

sudo nixos-rebuild switch --flake .#nixos-host

To confirm the app and the proxy are healthy, check the unit and curl the loopback port.

systemctl status hello-web
curl http://127.0.0.1:8080

A project dev shell

A dev shell gives everyone who clones a project the same toolchain without installing anything globally. This one sets up Node with pnpm, but the pattern is the same for any stack. Put the packages your project builds with into mkShell.

# flake.nix
{
  description = "Project dev shell";
 
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 
  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      devShells.${system}.default = pkgs.mkShell {
        packages = [
          pkgs.nodejs_22
          pkgs.pnpm
        ];
 
        shellHook = ''
          echo "node $(node --version), pnpm $(pnpm --version)"
        '';
      };
    };
}

devShells.x86_64-linux.default is the output nix develop looks for when you do not name one. packages is the list of tools that land on the PATH inside the shell, and shellHook runs when you enter, which is handy for printing versions or exporting environment variables. Enter it from the project root.

nix develop

Typing that every time gets old, so pair it with direnv . Drop a one-line .envrc next to the flake and the shell loads the moment you cd into the directory and unloads when you leave.

echo "use flake" > .envrc
direnv allow

A combined NixOS plus Home Manager flake

Managing the system and a user's dotfiles from one nixos-rebuild is the cleanest setup for a personal machine. Wire Home Manager in as a NixOS module and a single switch applies both. The follows line makes Home Manager reuse the exact nixpkgs the system uses, which keeps one package set instead of two.

# flake.nix
{
  description = "NixOS with Home Manager";
 
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };
 
  outputs = { self, nixpkgs, home-manager, ... }: {
    nixosConfigurations.nixos-host = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        home-manager.nixosModules.home-manager
        {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.users.user = import ./home.nix;
        }
      ];
    };
  };
}
# home.nix
{ pkgs, ... }:
{
  home.username = "user";
  home.homeDirectory = "/home/user";
  home.stateVersion = "24.11";
 
  home.packages = [ pkgs.ripgrep pkgs.fd ];
 
  programs.git = {
    enable = true;
    userName = "Your Name";
    userEmail = "you@example.com";
  };
}

The module list now has three entries. The system config, the Home Manager NixOS module that adds the home-manager options, and an inline module that turns them on for the user named user. useGlobalPkgs shares the system package set, useUserPackages installs the user's packages into the system profile, and the per-user config lives in home.nix. One command builds and activates the whole thing.

sudo nixos-rebuild switch --flake .#nixos-host

Two hosts in one flake

A flake can describe a fleet. Here a laptop and a server share one common.nix through imports, so settings you want everywhere live in one place and each host adds only what is specific to it.

# flake.nix
{
  description = "Two hosts sharing common config";
 
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 
  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations = {
      laptop = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [ ./laptop.nix ];
      };
 
      server = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [ ./server.nix ];
      };
    };
  };
}
# common.nix
{ pkgs, ... }:
{
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  time.timeZone = "UTC";
  environment.systemPackages = [ pkgs.vim pkgs.git ];
  system.stateVersion = "24.11";
}
# laptop.nix
{ ... }:
{
  imports = [ ./common.nix ./hardware-configuration.nix ];
 
  networking.hostName = "laptop";
  networking.networkmanager.enable = true;
  services.xserver.enable = true;
}
# server.nix
{ ... }:
{
  imports = [ ./common.nix ./hardware-configuration.nix ];
 
  networking.hostName = "server";
  services.openssh.enable = true;
  networking.firewall.allowedTCPPorts = [ 22 ];
}

Each host module pulls in common.nix with imports, which is how NixOS merges configuration. The shared file sets things both machines want, and the per-host files diverge from there, with the laptop getting NetworkManager and a graphical session while the server gets SSH and an open port. Build each one by its own name.

sudo nixos-rebuild switch --flake .#laptop
sudo nixos-rebuild switch --flake .#server

On the server itself you would switch with .#server. To build a host config for a different machine without activating it locally, use nix build against its toplevel and copy the result over.

A custom package with an overlay

An overlay rewrites the package set. Use it to add a package that nixpkgs does not have, or to change how an existing one is built. This one does both. It adds a fresh script-based tool and turns on an optional feature of ripgrep, then installs both.

# configuration.nix (excerpt)
{ pkgs, ... }:
{
  nixpkgs.overlays = [
    (final: prev: {
      # A brand new package built from a shell script.
      weather = prev.writeShellApplication {
        name = "weather";
        runtimeInputs = [ prev.curl ];
        text = ''
          curl -fsS "https://wttr.in/''${1:-}"
        '';
      };
 
      # Rebuild an existing package with a different build flag.
      ripgrep = prev.ripgrep.override { withPCRE2 = true; };
    })
  ];
 
  environment.systemPackages = [ pkgs.weather pkgs.ripgrep ];
}

An overlay is a function of two arguments. prev is the package set before this overlay, and final is the set after every overlay has applied, which is what lets overlays refer to each other's results. Adding weather is just binding a new attribute. Changing ripgrep uses .override, which re-evaluates the package with new arguments, here flipping on PCRE2 regex support. When the change you need is to the derivation itself rather than its arguments, reach for overrideAttrs instead.

nixpkgs.overlays = [
  (final: prev: {
    hello = prev.hello.overrideAttrs (old: {
      pname = "hello-greeting";
      doCheck = false;
    });
  })
];

.override changes the inputs a package was called with, while overrideAttrs patches the fields of the build itself, such as its name, sources, or build phases. Apply the overlay by rebuilding the host that imports the module.

sudo nixos-rebuild switch --flake .#nixos-host

Next

  • Flakes explains inputs, the lock file, and how reproducibility works.
  • Home Manager covers per-user packages, dotfiles, and services in depth.
  • Using a config walks through applying a flake to a machine and rolling back.
  • Editing your config shows how to change what is installed and configured.