Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,5 @@ nav:
- Nginx: examples/nginx.md
- Nginx HTTPS: examples/nginx-https.md
- Custom App: examples/custom-app.md
- Home Manager: examples/home-manager.md
- FAQ: faq.md
127 changes: 127 additions & 0 deletions docs/site/examples/home-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Home Manager

This example demonstrates how to manage per-user dotfiles and packages with
[Home Manager](https://github.com/nix-community/home-manager) alongside a
System Manager configuration.
Declare both in the same flake and a single `system-manager switch` will
activate the system-level configuration and run Home Manager for each
configured user.
See the [Home Manager manual](https://nix-community.github.io/home-manager/)
for the full catalogue of options and programs Home Manager can manage.

## Configuration

### flake.nix

```nix
{
description = "System Manager with Home Manager";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
system-manager = {
url = "github:numtide/system-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};

outputs =
{
self,
nixpkgs,
system-manager,
home-manager,
...
}:
{
systemConfigs.default = system-manager.lib.makeSystemConfig {
modules = [
home-manager.nixosModules.home-manager
(
{ pkgs, ... }:
{
nixpkgs.hostPlatform = "x86_64-linux";

# Userborn creates the user on activation.
services.userborn.enable = true;

# Required so home-manager can invoke nix-store/nix-build
# during activation.
nix.enable = true;

users.groups.alice.gid = 5000;
users.users.alice = {
isNormalUser = true;
uid = 5000;
group = "alice";
home = "/home/alice";
createHome = true;
};

home-manager = {
# Share the system's pkgs (and nixpkgs config) with Home Manager.
useGlobalPkgs = true;
# Install per-user packages into /etc/profiles/per-user/<name>.
useUserPackages = true;
# Back up any pre-existing dotfiles home-manager would overwrite.
backupFileExtension = "bak";

users.alice =
{ pkgs, ... }:
{
home.stateVersion = "24.05";

home.packages = [ pkgs.ripgrep ];

home.file.".config/example/hello.txt".text = ''
Hello from home-manager!
'';

programs.git = {
enable = true;
userName = "Alice";
userEmail = "alice@example.com";
};
};
};
}
)
];
};
};
}
```

## Usage

```bash
# Activate system-manager; home-manager runs per user during activation.
nix run 'github:numtide/system-manager' -- switch --flake /path/to/this/example --sudo
```

The per-user systemd unit `home-manager-<username>.service` runs once per
activation.
Check its status with:

```bash
systemctl status home-manager-alice.service
```

The activated dotfiles and generation symlinks live under
`/home/alice/.config/` and `/home/alice/.local/state/nix/profiles/`.

## Notes

- Users referenced in `home-manager.users.<name>` must also exist in
`users.users.<name>`; Home Manager reads `home` and the resolved username
from the system's user database.
- `useGlobalPkgs = true` disables Home Manager's own `nixpkgs.*` options and
reuses the instance from System Manager.
- `useUserPackages = true` makes `home.packages` available on the user's
login shell via `/etc/profiles/per-user/<username>/bin`.
- Home Manager activation requires a working Nix setup on the host
(`nix.enable = true` or an equivalent multi-user Nix install).
4 changes: 4 additions & 0 deletions docs/site/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ Extend the Nginx configuration with SSL/TLS certificates for HTTPS support, incl
### [Custom App](custom-app.md)

Deploy a custom TypeScript/Bun application behind Nginx, demonstrating how to fetch code from GitHub and run it as a systemd service. Includes a live example you can try.

### [Home Manager](home-manager.md)

Manage per-user dotfiles and packages with Home Manager in the same flake as your System Manager configuration, so a single activation covers both system and home.
19 changes: 19 additions & 0 deletions lib/container-test-driver/test_driver/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,25 @@ def copy_profile_to_container(self) -> None:
if result.returncode != 0:
msg = f"Failed to copy {store_path}: {result.stdout}"
raise Error(msg)
# Register the copied paths in the container's Nix DB so
# commands like `nix-store --realise` (used by home-manager
# activation) recognise them as valid store paths.
closure_basename = closure_info_path.name
# /run/host/nix is the bind-mounted host store
# the registration file is generated from https://github.com/NixOS/nixpkgs/blob/a4f11cb2b676c5c134bff4d8fab3d086ca78b0da/pkgs/build-support/closure-info.nix
registration = (
f"/run/host/nix/{closure_basename}/registration"
Comment thread
picnoir marked this conversation as resolved.
)
result = self.execute(
f"nix-store --load-db < {registration}",
timeout=120,
)
if result.returncode != 0:
msg = (
"Failed to register closure with Nix DB: "
f"{result.stdout}"
)
raise Error(msg)
return

# Fallback: try direct copy of the profile path
Expand Down
17 changes: 17 additions & 0 deletions nix/modules/upstream/nixpkgs/default.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
nixosModulesPath,
lib,
pkgs,
...
}:
{
Expand Down Expand Up @@ -52,5 +53,21 @@
default = "";
};

# Stubs for home-manager
system.userActivationScripts = lib.mkOption {
type = lib.types.attrsOf lib.types.unspecified;
default = { };
};

fonts.fontconfig.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};

i18n.glibcLocales = lib.mkOption {
type = lib.types.package;
default = pkgs.glibcLocales;
defaultText = lib.literalExpression "pkgs.glibcLocales";
};
};
}
2 changes: 2 additions & 0 deletions testFlake/container-tests/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
nixpkgs,
sops-nix,
system-manager-v1-1-0,
home-manager,
}:

let
Expand Down Expand Up @@ -71,6 +72,7 @@ let
hostPkgs
sops-nix
system-manager-v1-1-0
home-manager
;
};
testFiles = lib.filterAttrs (name: type: name != "default.nix" && lib.hasSuffix ".nix" name) (
Expand Down
72 changes: 72 additions & 0 deletions testFlake/container-tests/home-manager.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Test integration of the home-manager NixOS module with system-manager.
{
forEachDistro,
home-manager,
...
}:

forEachDistro "home-manager" {
modules = [
home-manager.nixosModules.home-manager
(
{ pkgs, ... }:
{
nix.enable = true;
services.userborn.enable = true;

users.groups.hmuser.gid = 5000;

users.users.hmuser = {
isNormalUser = true;
uid = 5000;
group = "hmuser";
home = "/home/hmuser";
createHome = true;
initialPassword = "hmuser";
};

home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "bak";
users.hmuser =
{ pkgs, ... }:
{
home.stateVersion = "24.05";
home.file.".config/system-manager-test/hello.txt".text = "hello from home-manager";
home.packages = [ pkgs.hello ];
};
};
}
)
];
testScriptFunction =
{ toplevel, ... }:
''
start_all()

machine.wait_for_unit("multi-user.target")

activation_logs = machine.activate()
for line in activation_logs.split("\n"):
assert not "ERROR" in line, line
machine.wait_for_unit("system-manager.target")

with subtest("home-manager per-user service is present"):
machine.wait_for_unit("home-manager-hmuser.service")
status = machine.succeed("systemctl is-active home-manager-hmuser.service").strip()
assert status == "active", f"Expected home-manager-hmuser.service active, got {status!r}"

with subtest("home-manager activated the managed file"):
content = machine.succeed("cat /home/hmuser/.config/system-manager-test/hello.txt").strip()
assert content == "hello from home-manager", f"Unexpected file content: {content!r}"

with subtest("home-manager user package is on the user's PATH"):
resolved = machine.succeed(
"runuser -u hmuser -- bash -lc 'command -v hello'"
).strip()
assert "/etc/profiles/per-user/hmuser/bin/hello" == resolved, (
f"Expected hello from per-user profile, got: {resolved!r}"
)
'';
}
21 changes: 21 additions & 0 deletions testFlake/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions testFlake/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
};
sops-nix.url = "github:Mic92/sops-nix";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};

outputs =
Expand All @@ -26,6 +28,7 @@
nix-vm-test,
sops-nix,
system-manager-v1-1-0,
home-manager,
}:
let
testedSystems = [
Expand Down Expand Up @@ -57,6 +60,7 @@
inherit system-manager;
inherit sops-nix;
inherit system-manager-v1-1-0;
inherit home-manager;
};
in
{
Expand Down