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-hostA 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-hostTo 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:8080A 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 developTyping 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 allowA 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-hostTwo 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 .#serverOn 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-hostNext
- 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.