Here are some snippets of code required to configure and run
attic on macOS with
nix-darwin
, and use it as an optional
cache.
Files required
To make nix-darwin
configure attic correctly, you need a configuration
module, which is called attic.nix
here, and a configuration atticd.toml
for
attic to use during runtime to use the correct paths for storage.
User creation
First, we create a user and group called attic
to host the attic server on
your machine. In the attic.nix
file, add the following:
# attic.nix
{ config, pkgs, ... }:
{
users.groups.attic = {
# Adjust the gid to your liking
gid = 603;
};
users.users.attic = {
createHome = false;
description = "attic user";
gid = 603;
# Adjust the uid to your liking
uid = 603;
isHidden = true;
};
users.knownGroups = [ "attic" ];
users.knownUsers = [ "attic" ];
}
Using the above, the attic user will be hidden, and can’t log in (shell set to
sbin/nologin
).
Attic configuration
We create a configuration file atticd.toml
, and put the following contents in
there:
# atticd.toml
# Socket address to listen on, you might want to adjust the port used.
listen = "127.0.0.1:18080"
# Optionally, configure allowed hosts here
allowed-hosts = []
[database]
# Attic's database is located in /var/attic/db.sqlite
url = "sqlite:///var/attic/db.sqlite?mode=rwc"
# Whether to enable sending on periodic heartbeat queries
#
# If enabled, a heartbeat query will be sent every minute
#heartbeat = false
[storage]
# Store everything locally in /var/attic/storage
type = "local"
path = "/var/attic/storage"
# Default values from
# https://github.com/zhaofengli/attic/blob/main/server/src/config-template.toml
[chunking]
nar-size-threshold = 65536 # chunk files that are 64 KiB or larger
min-size = 16384 # 16 KiB
avg-size = 65536 # 64 KiB
max-size = 262144 # 256 KiB
[compression]
type = "zstd"
#level = 8
[garbage-collection]
# The frequency to run garbage collection at
interval = "12 hours"
The configuration is written to /etc/attic/atticd.toml
using the following
Nix snippet:
# attic.nix
{ config, pkgs, ... }:
{
# ...
environment.etc = {
atticd = {
source = ./atticd.toml;
target = "attic/atticd.toml";
};
};
# ...
}
Credentials file
Next, feed 32 bytes of random data into base64
, and store them in a secret,
read-only file only accessible by attic
:
openssl rand 32 |
base64 |
sudo tee /etc/attic/secret.base64 > /dev/null
sudo chown attic:attic /etc/attic/secret.base64
sudo chmod 400 /etc/attic/secret.base64
Attic service file
Next, we need to tell nix-darwin
to install a launchd service using the
following snippet in the same attic.nix
file:
# attic.nix
{ config, pkgs, ... }:
let
logPath = "/var/log/atticd";
attic-client = pkgs.attic-client;
attic-server = pkgs.attic-server;
in
{
environment.systemPackages = [
attic-client
attic-server
];
launchd.daemons.attic = {
script = ''
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="$(cat /etc/attic/secret.base64)"
export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64
exec ${attic-server}/bin/atticd --config /etc/attic/atticd.toml
'';
serviceConfig = {
KeepAlive = true;
StandardOutPath = "${logPath}/attic.stdout.log";
StandardErrorPath = "${logPath}/attic.stderr.log";
UserName = "attic";
};
};
}
The above launchd daemon script reads the attic secret token into an
environment variable and starts the attic server using the configuration stored
in atticd.toml
.
Ensure that an attic runtime directory is created:
sudo mkdir -m700 /var/attic
sudo chown attic:attic /var/attic
Configuring the client
Import attic.nix
into your main nix-darwin
configuration:
# darwin-configuration.nix
{ config, pkgs, ... }:
{
imports = [
# ...
./attic.nix
# ...
];
}
Then, rebuild your nix-darwin
system using darwin-rebuild switch
. On my
system, I use the following invocation:
darwin-rebuild switch --flake $DOTFILES/nix/generic
Create a JWT for a cache named after your computer’s name to be used by your local Nix builder:
sudo -u attic \
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="$(sudo -u attic cat /etc/attic/secret.base64)" \
atticadm make-token \
--config /etc/attic/atticd.toml \
--sub "$(hostname)" \
--validity "1 month" \
--pull "$(hostname)-*" \
--push "$(hostname)-*" \
--delete "$(hostname)-*" \
--create-cache "$(hostname)-*" \
--configure-cache "$(hostname)-*" \
--configure-cache-retention "$(hostname)-*" \
--destroy-cache "$(hostname)-*"
The above JWT has very broad permissions, please adjust to your liking. A token will be output in your shell session.
Finally, try logging in with the generated token:
# Port configured in atticd.toml
attic login "$(hostname)" http://127.0.0.1:18080 "$YOUR_TOKEN"
Did it work? Great! In order for the Nix builder to be able to use attic as its
cache, it needs to have credentials available in a netrc
file, and have the
cache’s public key listed as a trusted key.
First, we retrieve the public key and netrc
file. The above attic login
invocation created a config.toml
file in $HOME/.config/attic
, which
conveniently contains the JWT token for a netrc
file.
# This will try to grab the netrc information that attic created after logging
# in
sed -n -E -e 's/token = "(.+)"/machine .+\npassword \1/p' \
$HOME/.config/attic/config.toml |
sudo tee /etc/nix/netrc
sudo chmod 440 /etc/nix/netrc
# Let me know if the above sed expression worked
The public key can be listed using attic cache info
:
attic cache info "$(hostname)-default"
This will output something like the following:
Public: false
Public Key: XXXXXXX-default:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Binary Cache Endpoint: https://XXXXXXXXXXXXX:18080/XXXXXXX-default
API Endpoint: https://XXXXXXXXXXXXX:18080/
Store Directory: /nix/store
Priority: 41
Upstream Cache Keys: ["cache.nixos.org-1"]
Retention Period: Global Default
We can then combine the above information and put it into attic.nix
:
# attic.nix
{ ... } :
let
# make sure to insert the correct hostname here:
hostname = "your-hostname";
# Insert the public key from above
public-key = "";
# Make sure the hostname, port, and cache name are correct
cache-url = "http://127.0.0.1:18080/${hostname}-default";
in
{
nix.settings.substituters = [ cache-url ];
nix.settings.trusted-public-keys = [
"${hostname}-default:${public-key}"
];
nix.settings.trusted-substituters = [ cache-url ];
# This file was created using the sed script above
nix.settings.netrc-file = "/etc/nix/netrc";
}
Testing the cache
Now, rebuild nix-darwin
one more time. Every time you run nix commands after
that, the local attic cache will be consulted first. Try it with any command:
# This will look up hello in your local cache first
nix run nixpkgs#hello
Troubleshooting
Does Nix complain that your local cache is not a binary cache? Check that you can access the attic cache using curl first:
# Might have copy the netrc file somewhere user-readable
curl --netrc-file /etc/nix/netrc -v -n \
"http://localhost:18080/$(hostname)-default/nix-cache-info"
Normally, the result will look like this:
[...]
< content-type: text/x-nix-cache-info
< date: Sat, 07 Sep 2024 08:15:50 GMT
< content-length: 51
<
WantMassQuery: 1
StoreDir: /nix/store
Priority: 41
[...]
This way, you can see if the credentials are correct or not, and if attic is accessible at all.
Furthermore, monitor the attic logs under /var/log/atticd
for any error
messages. A normal log output will look like this:
==> /var/log/atticd/attic.stderr.log <==
[...]
Attic Server 0.1.0 (release)
Running migrations...
Starting API server...
Listening on 127.0.0.1:18080...
==> /var/log/atticd/attic.stdout.log <==
Further reading
nix-darwin
: https://github.com/LnL7/nix-darwin- attic source code repository: https://github.com/zhaofengli/attic
- attic NixOS configuration: https://github.com/zhaofengli/attic/blob/main/nixos/atticd.nix
- How to serve and configure Nix store (not cache) via HTTP: https://nix.dev/manual/nix/2.18/package-management/binary-cache-substituter