내가 NixOS를 (선언적으로) 설치하는 방법
네트워크 스토리지 PC 빌드 중 하나를 위해 Flatcar Container Linux의 대안을 찾다가 NixOS를 거의 10년 만에 다시 써 보게 되었습니다(이전에는 CoreOS를 사용했습니다). NixOS를 설치하는 방법은 여러 가지가 있는데, 이 글에서는 제가 물리 머신이나 가상 머신에 NixOS를 설치할 때 선호하는 방법, 즉 네트워크를 통해 완전히 선언적으로 설치하는 방법을 정리하겠습니다.
소개: 선언적이라는 게 뭔가요?
선언적이라는 용어는 어떻게 할지가 아니라 무엇을 달성할지를 기술한다는 뜻입니다. NixOS에서는 apt install 같은 명령을 실행하는 대신, 시스템에 포함하고 싶은 소프트웨어를 선언합니다(설정 옵션인 environment.systemPackages에 추가하거나 모듈을 활성화하는 방식입니다).
선언적 접근 방식의 좋은 점은 시스템이 설정을 그대로 따른다는 것입니다. 그래서 설정을 되돌리면 시스템에 가해진 변경도 깔끔하게 되돌릴 수 있습니다.
저는 선언적 설정 파일을 보통 Git으로 버전 관리하는 것을 선호합니다.
지금 쓰고 있는 네트워크 스토리지 시스템을 처음 구성할 때는 cloud-init 기반의 선언적 설정과 자동으로 업데이트되는 기본 시스템을 갖춘 CoreOS(나중에는 Flatcar Container Linux)를 선택했습니다.
NixOS 설치 방법
그래픽 설치 프로그램: 데스크톱 전용
NixOS 매뉴얼의 “Installation” 섹션에는 그래픽 설치 프로그램(“데스크톱 사용자용”이며, Calamares 시스템 설치 프로그램을 기반으로 2022년에 추가되었습니다)과 수동 설치 프로그램이 소개되어 있습니다.
그래픽 설치 프로그램을 쓰면 디스크에 NixOS를 설치하는 과정이 쉽습니다. 기본값을 계속 확인만 눌러 주면 동작하는 시스템이 완성됩니다. 하지만 몇 가지 단점이 있습니다.
- 설치가 끝난 뒤 SSH를 수동으로 활성화해야 합니다. 네트워크가 아닌 로컬에서 직접 해야 합니다.
- 그래픽 설치 프로그램이 초기 NixOS 설정을 자동으로 생성해 주지만, 자신만의 초기 설정을 주입할 방법은 없습니다.
그래픽 설치 프로그램은 원격 설치나 자동화 설치를 염두에 둔 도구가 아님은 분명합니다.
수동 설치
반면 수동 설치 프로그램은 제 취향에는 너무 수동적입니다. NixOS 매뉴얼의 Installation summary 섹션에서 “Example 2”와 “Example 3”을 펼쳐 보면 감이 오실 겁니다. 물론 단계 자체는 충분히 따라 할 만합니다. 하지만 급하게 시스템을 설치해야 할 때 이런 방식으로 하고 싶지는 않습니다. 우선 수동 절차는 부담이 큰 상황에서 실수하기 쉽습니다. 그리고 대화형으로 명령을 복사해 붙여 넣는 것은 선언적 설정 파일을 작성하는 것과 정반대이기도 합니다.
네트워크 설치: nixos-anywhere
이상적으로는 설치 과정 대부분을 제 PC에서 편하게 진행하고 싶습니다. 즉, 설치 프로그램이 네트워크를 통해 사용할 수 있어야 합니다. 또 설치 직후 별도의 수동 단계 없이 바로 동작하는 초기 NixOS 설정으로 시스템이 올라오기를 바랍니다.
다행히 (커뮤니티에서 제공하는) 해결책이 있습니다. 바로 nixos-anywhere입니다. NixOS 설치 프로그램으로 부팅하는 것만 직접 처리하면, 이후 명령어 한 번으로 nixos-anywhere가 해당 설치 프로그램에 SSH로 접속해 디스크를 파티셔닝하고 NixOS를 디스크에 설치해 줍니다. 특히 nixos-anywhere는 선언적으로 설정되므로 이 단계를 언제든 반복할 수 있습니다.
(nixos-anywhere가 임의의 시스템에 SSH로 접속한 뒤 kexec로 재부팅해 NixOS 설치 프로그램으로 진입시킬 수 있다는 것도 알고 있습니다. 확실히 멋진 기술이지만, 저는 설치 프로그램을 직접 부팅하는 방식이 더 안전하고 범용적이며 재현 가능하다고 느껴 그쪽을 선호합니다.)
준비: Nix 설치
저는 NixOS를 머신 하나에 쓰고 싶었지만, (현재로서는) 메인 데스크톱 PC에서는 쓸 생각이 없었습니다.
그래서 Arch Linux에는 NixOS를 구동하지 않고도 빌드할 수 있도록 nix 도구만 설치했습니다.
% sudo pacman -S nix
% sudo groupadd -r nixbld
% for n in $(seq 1 24); do sudo useradd -c "Nix build user $n" \
-d /var/empty -g nixbld -G nixbld -M -N -r -s "$(which nologin)" \
nixbld$n; done
% sudo systemctl enable --now nix-daemon.socket이제 nix-shell -p hello를 실행하면 GNU hello 패키지가 설치된 새 셸로 진입해야 합니다.
% export NIX_PATH=nixpkgs=channel:nixos-25.05
% nix-shell -p hello
hello
[nix-shell:/tmp]$ hello
Hello, world!참고로 Arch Linux 위키의 Nix 페이지에는 nix로 패키지를 설치하는 방법이 설명되어 있지만, 제가 관심 있는 부분은 아닙니다. 저는 NixOS 시스템을 원격으로 관리하려는 것뿐입니다.
직접 설치 프로그램 빌드하기
앞서 “NixOS 설치 프로그램으로 부팅하는 건 직접 처리한다”고 말씀드렸는데, 이는 그리 어렵지 않습니다. ISO 이미지를 USB 스틱에 쓰고 해당 기기로 부팅하면 됩니다(또는 VM에서 ISO를 선택해 부팅하면 됩니다).
하지만 SSH로 원격 로그인을 하려면 먼저 수동으로 비밀번호를 설정해야 합니다. 또 제가 주로 쓰는 터미널인 rxvt-unicode의 termcap 파일이 기본 NixOS 설치 환경에 포함되어 있지 않아 TERM=xterm 환경 변수로 SSH 접속을 해야 했습니다. 마찬가지로 제가 설정한 로캘이 동작하지 않고, 선호하는 셸인 Zsh도 사용할 수 없습니다.
설치 프로그램이 애초에 편한 환경으로 미리 설정되어 있다면 훨씬 좋지 않을까요?
Debian이나 Fedora, Arch Linux 같은 다른 리눅스 배포판이었다면 공식 설치 ISO 이미지를 다시 빌드하려고 시도하지 않았을 겁니다. 그쪽의 프로세스와 도구가 잘 동작하리라는 건 알지만, 새로 배워서 디버깅하고 유지 관리해야 할 일이 하나 더 늘어나는 셈이기 때문입니다.
하지만 NixOS 설치 프로그램을 빌드하는 과정은 일반 NixOS 시스템을 설정하는 것과 매우 유사합니다. 같은 설정, 같은 빌드 도구를 씁니다. 절차는 공식 NixOS 위키에 문서화되어 있습니다.
저는 보통 configuration.nix에 넣는 커스터마이징을 그대로 가져와 nixpkgs의 installation-cd-minimal.nix 모듈을 불러온 뒤, 그 결과를 iso.nix 파일에 담았습니다.
{ config, pkgs, ... }:
{
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
<nixpkgs/nixos/modules/installer/cd-dvd/channel.nix>
];
i18n.supportedLocales = [
"en_DK.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"de_CH.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
i18n.defaultLocale = "en_US.UTF-8";
security.sudo.wheelNeedsPassword = false;
users.users.michael = {
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key"
];
isNormalUser = true;
description = "Michael Stapelberg";
extraGroups = [ "wheel" ];
initialPassword = "SGZ3odMZIesxTuh2Y2pUaJA"; # random for this post
shell = pkgs.zsh;
packages = with pkgs; [];
};
environment.systemPackages = with pkgs; [
git # for checking out github.com/stapelberg/configfiles
rsync
zsh
vim
emacs
wget
curl
rxvt-unicode # for terminfo
lshw
];
programs.zsh.enable = true;
services.openssh.enable = true;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "25.05"; # Did you read the comment?
}ISO 이미지를 빌드하기 위해 NIX_PATH 환경 변수를 설정해 nix-build(1)이 iso.nix 파일을 참조하도록 하고, NixOS 25.05의 업스트림 채널을 선택했습니다.
% export NIX_PATH=nixos-config=$PWD/iso.nix:nixpkgs=channel:nixos-25.05
% nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage제 2025년형 하이엔드 리눅스 PC에서 약 1.5분이 지나자 설치 ISO는 result/iso/nixos-minimal-25.05.802216.55d1f923c480-x86_64-linux.iso 경로에서 찾을 수 있었습니다(제 경우 크기는 1.46GB였습니다).
Nix Flakes 활성화하기
안타깝게도 nix 프로젝트는 5년 넘게 제공된 “실험적” 신규 커맨드 라인 인터페이스(CLI)를 아직 기본값으로 활성화하지 못했기 때문에, 설정 파일을 만들고 최신 nix-command 인터페이스를 직접 활성화해야 합니다.
% mkdir -p ~/.config/nix
% echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf신구를 어떻게 구분할까요? 기존 명령은 하이픈으로 연결되고(nix-build), 신규 명령은 공백으로 구분됩니다(nix build).
저는 Nix flakes도 함께 활성화했는데, 이를 통해 nix 빌드를 hermetic하게 만들고 nixpkgs나 빌드에 포함하려는 다른 nix 모듈의 특정 리비전에 고정할 수 있습니다. 저는 flakes를 다른 프로그래밍 환경의 버전 잠금 파일에 비유하곤 합니다. 5개월 뒤에 시스템을 빌드해도 오늘과 같은 결과가 나온다는 것이 핵심 아이디어입니다.
flakes가 동작하는지 확인하려면 nix shell(nix-shell이 아닙니다)을 실행해 보십시오.
% nix shell nixpkgs#hello
/tmp 2 % hello
Hello, world!(재)설치 단계
참고로 Proxmox에서 NixOS용 새 VM을 만들 때 제가 사용하는 설정은 다음과 같습니다. 가장 중요한 설정은 bios=ovmf(= UEFI 부팅, 기본값이 아닙니다)로, 이를 통해 물리 머신과 VM에서 동일한 부트로더 설정을 사용할 수 있습니다.

(서명되지 않은) 설치 프로그램으로 부팅하려면 먼저 UEFI 설정으로 들어가 Secure Boot를 비활성화해야 합니다. 참고로 Proxmox는 기본적으로 Secure Boot를 활성화합니다.
그런 다음 대상 시스템에서 커스텀 설치 ISO로 부팅하고, ssh [email protected]이 비밀번호 입력 없이 동작하는지 확인합니다.
다음 내용으로 flake.nix를 작성합니다.
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
disko.url = "github:nix-community/disko";
# Use the same version as nixpkgs
disko.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
nixpkgs,
disko,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = false;
};
in
{
nixosConfigurations.zammadn = nixpkgs.lib.nixosSystem {
inherit system;
inherit pkgs;
modules = [
disko.nixosModules.disko
./configuration.nix
];
};
formatter.${system} = pkgs.nixfmt-tree;
};
}disk-config.nix에 디스크 설정을 선언합니다.
disk-config.nix
{ lib, ... }:
{
disko.devices = {
disk = {
main = {
device = lib.mkDefault "/dev/sda";
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
type = "EF00";
size = "500M";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
}원하는 NixOS 설정을 configuration.nix에 선언합니다.
{ modulesPath, lib, pkgs, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
./hardware-configuration.nix
./disk-config.nix
];
# Adding michael as trusted user means
# we can upgrade the system via SSH (see Makefile).
nix.settings.trusted-users = [ "michael" "root" ];
# Clean the Nix store every week.
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 10;
};
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "zammadn";
time.timeZone = "Europe/Zurich";
# Use systemd for networking
services.resolved.enable = true;
networking.useDHCP = false;
systemd.network.enable = true;
systemd.network.networks."10-e" = {
matchConfig.Name = "e*"; # enp9s0 (10G) or enp8s0 (1G)
networkConfig = {
IPv6AcceptRA = true;
DHCP = "yes";
};
};
i18n.supportedLocales = [
"en_DK.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"de_CH.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
i18n.defaultLocale = "en_US.UTF-8";
users.mutableUsers = false;
security.sudo.wheelNeedsPassword = false;
users.users.michael = {
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key"
];
isNormalUser = true;
description = "Michael Stapelberg";
extraGroups = [ "networkmanager" "wheel" ];
initialPassword = "install"; # TODO: change!
shell = pkgs.zsh;
packages = with pkgs; [];
};
environment.systemPackages = with pkgs; [
git # for checking out github.com/stapelberg/configfiles
rsync
zsh
vim
emacs
wget
curl
];
programs.zsh.enable = true;
services.openssh.enable = true;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "25.05"; # Did you read the comment?
}…그리고 잠급니다.
% nix flake lock- nixos-anywhere를 이용해 설치 프로그램에서 hardware-configuration.nix를 받아오고 NixOS를 디스크에 설치합니다.
% nix run github:nix-community/nixos-anywhere -- \
--flake .#zammadn \
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
--target-host [email protected]약 1분 뒤, VM 설치가 완료되고 재부팅되었습니다!
전체 nixos-anywhere 설치 로그 (궁금하신 분을 위해)
% nix run github:nix-community/nixos-anywhere -- \
--flake .#wiki \
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
--target-host [email protected]
[... transcript truncated for brevity but contains full nixos-anywhere log ...]
### Installing NixOS ###
installing the boot loader...
setting up /etc...
Created "/boot/EFI".
installation finished!
### Rebooting ###
### Done! ###설치 후 단계
이제 시스템의 선언적 부분이 갖춰졌으니, 상태를 가지는 부분을 처리해야 합니다.
제 경우에는 설정이 필요한 상태 기반 요소는 Tailscale 메시 VPN뿐입니다.
Tailscale을 설정하려면 SSH로 로그인해 sudo tailscale up을 실행합니다. 그런 다음 표시되는 링크를 따라 새 노드를 네트워크에 추가합니다. 이후 Tailscale Machines 콘솔에서 키 만료를 비활성화하고 ACL 태그를 추가합니다.
변경 사항 적용하기
이제 설정 파일을 수정한 뒤에는 nixos-rebuild를 원격으로 실행해 변경 사항을 NixOS 시스템에 배포합니다.
% nix run nixpkgs#nixos-rebuild -- \
--target-host michael@zammadn \
--use-remote-sudo \
switch \
--flake .#zammadn모든 변경이 nixos-rebuild switch로 완전히 적용되는 것은 아니라는 점에 유의하십시오. systemd 서비스는 대체로 재시작되지만, 새로 필요한 커널 모듈은 자동으로 로드되지 않습니다(예: Frigate에서 edgetpu Coral 하드웨어 가속기를 활성화한 경우).
따라서 변경 사항을 배포한 뒤에는 모든 것이 제대로 적용되도록 시스템을 재부팅하는 것이 좋습니다.
NixOS의 장점 중 하나는 부팅 메뉴에서 실행할 시스템 세대를 선택할 수 있다는 점입니다. 최근 변경으로 문제가 생겼다면 빠르게 이전 세대로 재부팅해 변경을 되돌릴 수 있습니다. 물론 설정 변경 자체를 되돌리고 새 세대를 배포할 수도 있습니다. 상황에 더 편한 방법을 선택하면 됩니다.
결론
이 글을 통해 Nix와 NixOS를 처음 시작할 때 누군가 알려줬으면 했던 내용을 전달할 수 있었기를 바랍니다.
- flakes와 새로운 CLI를 활성화하십시오.
- 원격 설치에는 nixos-anywhere를 사용하십시오.
- 원한다면 커스텀 설치 프로그램을 빌드하십시오. 정말 쉽습니다!
nixos-rebuild에 내장된--target-host플래그를 원격 배포에 사용하십시오.
이제 어디로 가면 좋을까요?
- nixos.org → Learn에 있는 모든 문서를 읽어 보십시오.
- 참고 및 영감을 얻기 위해 살펴본, 제 주변 사람들의 글 몇 가지를 순서 없이 소개합니다.
- Michael Lynch는 NixOS로 Oracle Cloud VM 설정하기와 자신의 Zig 설정 관리하기에 대해 썼습니다.
- Nelson Elhage는 Python 3.14 꼬리 호출 인터프리터 성능에 대한 성능 연구의 일환으로 수십 개의 Python 인터프리터를 테스트하는 데 Nix를 활용한 경험을 썼습니다.
- Vincent Bernat는 ARM 싱글 보드 컴퓨터용 SD 카드 이미지를 Nix로 빌드하기에 대해 썼습니다.
- Mitchell Hashimoto는 자신의 방대한 NixOS 설정을 공유했습니다.
- Wolfgang은 홈 서버에 NixOS를 사용하는 방법에 대한 YouTube 영상을 공개했습니다(→ 그의 설정)
- 가까운 Nix 커뮤니티에 연락해 보십시오! 저는 최근 Nix Zürich 그룹의 “Zero Hydra Failures” 행사에 참석했는데, 그곳의 친절한 분들이 Nix에 관한 모든 이야기를 기꺼이 나눠 주셨습니다 :)
글을 무작위로 읽기