diff --git a/.gitignore b/.gitignore
index 35568021..eaf85810 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ target
bench/bench_workspace
zerofs.toml
TODO.md
+result*
diff --git a/README.md b/README.md
index b80b71ec..37fdbb9b 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,39 @@ cargo install zerofs
docker pull ghcr.io/barre/zerofs:latest
```
+#### Via Nix
+```nix
+# flake.nix
+{
+ inputs.zerofs.url = "github:Barre/ZeroFS";
+
+ outputs = { nixpkgs, zerofs, ... }: {
+ nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
+ modules = [
+ { nixpkgs.overlays = [ zerofs.overlays.default ]; }
+ zerofs.nixosModules.default
+ {
+ services.zerofs = {
+ enable = true;
+ settings = {
+ cache.dir = "/var/cache/zerofs";
+ cache.disk_size_gb = 10.0;
+ storage.url = "s3://my-bucket/zerofs-data";
+ storage.encryption_password = "\${ZEROFS_PASSWORD}";
+ servers.nfs.addresses = [ "127.0.0.1:2049" ];
+ aws = {
+ access_key_id = "\${AWS_ACCESS_KEY_ID}";
+ secret_access_key = "\${AWS_SECRET_ACCESS_KEY}";
+ };
+ };
+ };
+ }
+ ];
+ };
+ };
+}
+```
+
### Getting Started
```bash
diff --git a/documentation/src/app/quickstart/page.mdx b/documentation/src/app/quickstart/page.mdx
index b57c5a42..b7307e5b 100644
--- a/documentation/src/app/quickstart/page.mdx
+++ b/documentation/src/app/quickstart/page.mdx
@@ -48,6 +48,23 @@ docker pull ghcr.io/barre/zerofs:latest
encryption-password: ${{ secrets.ZEROFS_PASSWORD }}
```
+```nix {{ title: 'NixOS Module' }}
+# flake.nix
+{
+ inputs.zerofs.url = "github:Barre/ZeroFS";
+
+ outputs = { nixpkgs, zerofs, ... }: {
+ nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
+ modules = [
+ { nixpkgs.overlays = [ zerofs.overlays.default ]; }
+ zerofs.nixosModules.default
+ { services.zerofs.enable = true; /* ... */ }
+ ];
+ };
+ };
+}
+```
+
## Configuration
@@ -139,6 +156,24 @@ allow_http = "true" # For HTTP endpoints
addresses = ["127.0.0.1:2049"]
```
+```nix {{ title: 'NixOS' }}
+services.zerofs = {
+ enable = true;
+ settings = {
+ cache.dir = "/var/cache/zerofs";
+ cache.disk_size_gb = 10.0;
+ cache.memory_size_gb = 2.0;
+ storage.url = "s3://my-bucket/zerofs-data";
+ storage.encryption_password = "\${ZEROFS_PASSWORD}";
+ servers.nfs.addresses = [ "127.0.0.1:2049" ];
+ aws = {
+ access_key_id = "\${AWS_ACCESS_KEY_ID}";
+ secret_access_key = "\${AWS_SECRET_ACCESS_KEY}";
+ };
+ };
+};
+```
+
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..0f2da68b
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,64 @@
+{
+ "nodes": {
+ "crane": {
+ "locked": {
+ "lastModified": 1766194365,
+ "narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1765835352,
+ "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "a34fae9c08a15ad73f295041fec82323541400a9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1766653575,
+ "narHash": "sha256-TPgxCS7+hWc4kPhzkU5dD2M5UuPhLuuaMNZ/IpwKQvI=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "3c1016e6acd16ad96053116d0d3043029c9e2649",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "crane": "crane",
+ "flake-parts": "flake-parts",
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..b72947cf
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,33 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
+
+ flake-parts = {
+ url = "github:hercules-ci/flake-parts";
+ inputs.nixpkgs-lib.follows = "nixpkgs";
+ };
+
+ crane.url = "github:ipetkov/crane";
+ };
+
+ outputs = inputs @ { self, flake-parts, ... }: let
+ inherit (inputs.nixpkgs) lib;
+
+ modules = builtins.foldl' (acc: f: f acc) ./nix/flake [
+ builtins.readDir
+ (lib.filterAttrs (name: type:
+ type == "regular" && lib.hasSuffix ".nix" name
+ ))
+ (lib.mapAttrsToList (name: _:
+ lib.path.append ./nix/flake name
+ ))
+ ];
+
+ in flake-parts.lib.mkFlake { inherit inputs; } {
+ imports = modules;
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ ];
+ };
+}
diff --git a/nix/flake/nixos-modules.nix b/nix/flake/nixos-modules.nix
new file mode 100644
index 00000000..fac6776a
--- /dev/null
+++ b/nix/flake/nixos-modules.nix
@@ -0,0 +1,7 @@
+{ inputs, ... }:
+{
+ flake.nixosModules = {
+ zerofs = import ../nixos/zerofs.nix;
+ default = inputs.self.nixosModules.zerofs;
+ };
+}
diff --git a/nix/flake/overlays.nix b/nix/flake/overlays.nix
new file mode 100644
index 00000000..ad404bc2
--- /dev/null
+++ b/nix/flake/overlays.nix
@@ -0,0 +1,12 @@
+{ inputs, ... }:
+{
+ flake.overlays = {
+ zerofs = final: prev: {
+ zerofs = final.callPackage "${inputs.self}/nix/package.nix" {
+ craneLib = inputs.crane.mkLib final;
+ };
+ };
+
+ default = inputs.self.overlays.zerofs;
+ };
+}
diff --git a/nix/flake/packages.nix b/nix/flake/packages.nix
new file mode 100644
index 00000000..c2e84e4f
--- /dev/null
+++ b/nix/flake/packages.nix
@@ -0,0 +1,13 @@
+{ inputs, ... }:
+{
+ perSystem = { pkgs, ... }:
+ let
+ craneLib = inputs.crane.mkLib pkgs;
+ zerofs = pkgs.callPackage "${inputs.self}/nix/package.nix" { inherit craneLib; };
+ in {
+ packages = {
+ inherit zerofs;
+ default = zerofs;
+ };
+ };
+}
diff --git a/nix/flake/tests.nix b/nix/flake/tests.nix
new file mode 100644
index 00000000..20237262
--- /dev/null
+++ b/nix/flake/tests.nix
@@ -0,0 +1,33 @@
+{ inputs, lib, ... }:
+{
+ perSystem =
+ { pkgs, system, ... }:
+ let
+ mkTest =
+ name:
+ lib.nixos.evalTest {
+ imports = [ (../nixos/tests + "/${name}.nix") ];
+ hostPkgs = pkgs;
+ defaults = {
+ imports = [ inputs.self.nixosModules.default ];
+ services.zerofs.package = lib.mkDefault inputs.self.packages.${system}.default;
+ };
+ };
+
+ tests = {
+ nfs = mkTest "nfs";
+ "9p-tcp" = mkTest "9p-tcp";
+ "9p-uds" = mkTest "9p-uds";
+ "nbd-zfs" = mkTest "nbd-zfs";
+ };
+ in
+ {
+ apps = lib.mapAttrs' (
+ name: test:
+ lib.nameValuePair "test-${name}" {
+ type = "app";
+ program = "${test.config.result.driver}/bin/nixos-test-driver";
+ }
+ ) tests;
+ };
+}
diff --git a/nix/nixos/tests/9p-tcp.nix b/nix/nixos/tests/9p-tcp.nix
new file mode 100644
index 00000000..e899a066
--- /dev/null
+++ b/nix/nixos/tests/9p-tcp.nix
@@ -0,0 +1,65 @@
+{ pkgs, ... }:
+{
+ name = "9p-tcp";
+
+ nodes.machine = { pkgs, ... }: {
+ services.minio = {
+ enable = true;
+ rootCredentialsFile = pkgs.writeText "minio-credentials" ''
+ MINIO_ROOT_USER=minioadmin
+ MINIO_ROOT_PASSWORD=minioadmin
+ '';
+ };
+
+ services.zerofs = {
+ enable = true;
+ settings = {
+ cache = {
+ dir = "/var/cache/zerofs";
+ disk_size_gb = 1.0;
+ };
+ storage = {
+ url = "s3://zerofs-test/data";
+ encryption_password = "test-password";
+ };
+ servers.ninep.addresses = [ "127.0.0.1:5564" ];
+ aws = {
+ access_key_id = "minioadmin";
+ secret_access_key = "minioadmin";
+ endpoint = "http://127.0.0.1:9000";
+ region = "us-east-1";
+ allow_http = "true";
+ };
+ };
+ };
+ };
+
+ testScript = ''
+ machine.wait_for_unit("minio.service")
+ machine.wait_for_open_port(9000)
+
+ machine.succeed(
+ "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin",
+ "${pkgs.minio-client}/bin/mc mb local/zerofs-test",
+ )
+
+ machine.wait_for_unit("zerofs.service")
+ machine.wait_for_open_port(5564)
+
+ # Mount zerofs via 9P over TCP
+ machine.succeed("mkdir -p /mnt/zerofs")
+ machine.succeed("mount -t 9p -o trans=tcp,port=5564,version=9p2000.L,cache=mmap 127.0.0.1 /mnt/zerofs")
+
+ # Test basic file operations
+ machine.succeed("echo 'Hello from ZeroFS 9P!' > /mnt/zerofs/test.txt")
+ output = machine.succeed("cat /mnt/zerofs/test.txt")
+ assert "Hello from ZeroFS 9P!" in output, f"Unexpected content: {output}"
+
+ machine.succeed("ls -la /mnt/zerofs/")
+
+ machine.succeed("rm /mnt/zerofs/test.txt")
+ machine.succeed("test ! -f /mnt/zerofs/test.txt")
+
+ machine.succeed("umount /mnt/zerofs")
+ '';
+}
diff --git a/nix/nixos/tests/9p-uds.nix b/nix/nixos/tests/9p-uds.nix
new file mode 100644
index 00000000..8890bcba
--- /dev/null
+++ b/nix/nixos/tests/9p-uds.nix
@@ -0,0 +1,67 @@
+{ pkgs, ... }:
+{
+ name = "9p-uds";
+
+ nodes.machine = { pkgs, ... }: {
+ services.minio = {
+ enable = true;
+ rootCredentialsFile = pkgs.writeText "minio-credentials" ''
+ MINIO_ROOT_USER=minioadmin
+ MINIO_ROOT_PASSWORD=minioadmin
+ '';
+ };
+
+ services.zerofs = {
+ enable = true;
+ settings = {
+ cache = {
+ dir = "/var/cache/zerofs";
+ disk_size_gb = 1.0;
+ };
+ storage = {
+ url = "s3://zerofs-test/data";
+ encryption_password = "test-password";
+ };
+ servers.ninep.unix_socket = "/run/zerofs/zerofs.9p.sock";
+ aws = {
+ access_key_id = "minioadmin";
+ secret_access_key = "minioadmin";
+ endpoint = "http://127.0.0.1:9000";
+ region = "us-east-1";
+ allow_http = "true";
+ };
+ };
+ };
+ };
+
+ testScript = ''
+ machine.wait_for_unit("minio.service")
+ machine.wait_for_open_port(9000)
+
+ machine.succeed(
+ "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin",
+ "${pkgs.minio-client}/bin/mc mb local/zerofs-test",
+ )
+
+ machine.wait_for_unit("zerofs.service")
+
+ # Wait for Unix socket to be created
+ machine.wait_for_file("/run/zerofs/zerofs.9p.sock")
+
+ # Mount zerofs via 9P over Unix domain socket
+ machine.succeed("mkdir -p /mnt/zerofs")
+ machine.succeed("mount -t 9p -o trans=unix,version=9p2000.L,cache=mmap,access=user /run/zerofs/zerofs.9p.sock /mnt/zerofs")
+
+ # Test basic file operations
+ machine.succeed("echo 'Hello from ZeroFS 9P UDS!' > /mnt/zerofs/test.txt")
+ output = machine.succeed("cat /mnt/zerofs/test.txt")
+ assert "Hello from ZeroFS 9P UDS!" in output, f"Unexpected content: {output}"
+
+ machine.succeed("ls -la /mnt/zerofs/")
+
+ machine.succeed("rm /mnt/zerofs/test.txt")
+ machine.succeed("test ! -f /mnt/zerofs/test.txt")
+
+ machine.succeed("umount /mnt/zerofs")
+ '';
+}
diff --git a/nix/nixos/tests/nbd-zfs.nix b/nix/nixos/tests/nbd-zfs.nix
new file mode 100644
index 00000000..be28700e
--- /dev/null
+++ b/nix/nixos/tests/nbd-zfs.nix
@@ -0,0 +1,126 @@
+{ pkgs, ... }:
+{
+ name = "nbd-zfs";
+
+ nodes.machine = { pkgs, ... }: {
+ services.minio = {
+ enable = true;
+ rootCredentialsFile = pkgs.writeText "minio-credentials" ''
+ MINIO_ROOT_USER=minioadmin
+ MINIO_ROOT_PASSWORD=minioadmin
+ '';
+ };
+
+ services.zerofs = {
+ enable = true;
+ settings = {
+ cache = {
+ dir = "/var/cache/zerofs";
+ disk_size_gb = 2.0;
+ memory_size_gb = 2.0;
+ };
+ storage = {
+ url = "s3://zerofs-test/data";
+ encryption_password = "test-password";
+ };
+ servers.ninep.addresses = [ "127.0.0.1:5564" ];
+ servers.nbd.addresses = [ "127.0.0.1:10809" ];
+ # Higher L0 limits needed for slow VM environments where compaction can't keep up
+ lsm = {
+ l0_max_ssts = 64;
+ max_concurrent_compactions = 32;
+ };
+ aws = {
+ access_key_id = "minioadmin";
+ secret_access_key = "minioadmin";
+ endpoint = "http://127.0.0.1:9000";
+ region = "us-east-1";
+ allow_http = "true";
+ };
+ };
+ };
+
+ boot.supportedFilesystems = [ "zfs" ];
+ boot.zfs.forceImportRoot = false;
+ boot.kernelModules = [ "nbd" ];
+ networking.hostId = "deadbeef";
+
+ environment.systemPackages = with pkgs; [
+ nbd
+ zfs
+ ];
+ };
+
+ testScript = ''
+ machine.wait_for_unit("minio.service")
+ machine.wait_for_open_port(9000)
+
+ machine.succeed(
+ "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin",
+ "${pkgs.minio-client}/bin/mc mb local/zerofs-test",
+ )
+
+ machine.wait_for_unit("zerofs.service")
+ machine.wait_for_open_port(5564)
+ machine.wait_for_open_port(10809)
+
+ # Mount 9P and create NBD device file
+ machine.succeed("mkdir -p /mnt/9p")
+ machine.succeed("mount -t 9p -o trans=tcp,port=5564,version=9p2000.L 127.0.0.1 /mnt/9p")
+ machine.succeed("mkdir -p /mnt/9p/.nbd")
+ machine.succeed("truncate -s 1G /mnt/9p/.nbd/test-device")
+ machine.succeed("umount /mnt/9p")
+
+ # Connect NBD device
+ machine.succeed("nbd-client 127.0.0.1 10809 /dev/nbd0 -N test-device")
+ machine.succeed("blockdev --getsize64 /dev/nbd0")
+
+ # Create ZFS pool and filesystem
+ machine.succeed("zpool create testpool /dev/nbd0")
+ machine.succeed("zfs create testpool/data")
+ machine.succeed("zfs set mountpoint=/mnt/zfsdata testpool/data")
+
+ # Write test data
+ machine.succeed("echo 'Hello from ZeroFS NBD+ZFS!' > /mnt/zfsdata/test.txt")
+ machine.succeed("dd if=/dev/urandom of=/mnt/zfsdata/random.dat bs=1M count=10")
+ original_checksum = machine.succeed("sha256sum /mnt/zfsdata/random.dat").strip()
+
+ # Create snapshot
+ machine.succeed("zfs snapshot testpool/data@snap1")
+
+ # Sync and export
+ machine.succeed("sync")
+ machine.succeed("zpool sync testpool")
+ machine.succeed("zfs unmount testpool/data")
+ machine.succeed("zpool export testpool")
+
+ # Disconnect NBD and wait for full disconnect
+ machine.succeed("nbd-client -d /dev/nbd0")
+ machine.wait_until_succeeds("test $(cat /sys/block/nbd0/size) = 0")
+ machine.wait_until_succeeds("! nbd-client -c /dev/nbd0")
+
+ # Restart zerofs
+ machine.succeed("systemctl stop zerofs.service")
+ machine.succeed("systemctl start zerofs.service")
+ machine.wait_for_unit("zerofs.service")
+ machine.wait_for_open_port(10809)
+
+ # Reconnect NBD and import pool
+ machine.succeed("nbd-client 127.0.0.1 10809 /dev/nbd0 -N test-device")
+ machine.succeed("zpool import testpool")
+
+ # Verify data integrity
+ output = machine.succeed("cat /mnt/zfsdata/test.txt")
+ assert "Hello from ZeroFS NBD+ZFS!" in output, f"Unexpected content: {output}"
+
+ restored_checksum = machine.succeed("sha256sum /mnt/zfsdata/random.dat").strip()
+ assert original_checksum == restored_checksum, f"Checksum mismatch: {original_checksum} vs {restored_checksum}"
+
+ # Verify snapshot exists
+ machine.succeed("zfs list -t snapshot testpool/data@snap1")
+
+ # Cleanup
+ machine.succeed("zpool export testpool")
+ machine.succeed("nbd-client -d /dev/nbd0")
+ '';
+}
diff --git a/nix/nixos/tests/nfs.nix b/nix/nixos/tests/nfs.nix
new file mode 100644
index 00000000..3d509dad
--- /dev/null
+++ b/nix/nixos/tests/nfs.nix
@@ -0,0 +1,74 @@
+{ pkgs, ... }:
+{
+ name = "nfs";
+
+ nodes.machine = { pkgs, ... }: {
+ # Minio for S3-compatible storage
+ services.minio = {
+ enable = true;
+ rootCredentialsFile = pkgs.writeText "minio-credentials" ''
+ MINIO_ROOT_USER=minioadmin
+ MINIO_ROOT_PASSWORD=minioadmin
+ '';
+ };
+
+ services.zerofs = {
+ enable = true;
+ settings = {
+ cache = {
+ dir = "/var/cache/zerofs";
+ disk_size_gb = 1.0;
+ };
+ storage = {
+ url = "s3://zerofs-test/data";
+ encryption_password = "test-password";
+ };
+ servers.nfs.addresses = [ "127.0.0.1:2049" ];
+ aws = {
+ access_key_id = "minioadmin";
+ secret_access_key = "minioadmin";
+ endpoint = "http://127.0.0.1:9000";
+ region = "us-east-1";
+ allow_http = "true";
+ };
+ };
+ };
+
+ # NFS client for mounting
+ environment.systemPackages = [ pkgs.nfs-utils ];
+ };
+
+ testScript = ''
+ machine.wait_for_unit("minio.service")
+ machine.wait_for_open_port(9000)
+
+ # Create the bucket for zerofs
+ machine.succeed(
+ "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin",
+ "${pkgs.minio-client}/bin/mc mb local/zerofs-test",
+ )
+
+ machine.wait_for_unit("zerofs.service")
+
+ # Wait for NFS to be ready
+ machine.wait_for_open_port(2049)
+
+ # Mount zerofs via NFS
+ machine.succeed("mkdir -p /mnt/zerofs")
+ machine.succeed("mount -t nfs -o async,nolock,rsize=1048576,wsize=1048576,tcp,port=2049,mountport=2049,hard 127.0.0.1:/ /mnt/zerofs")
+
+ # Test basic file operations
+ machine.succeed("echo 'Hello from ZeroFS!' > /mnt/zerofs/test.txt")
+ output = machine.succeed("cat /mnt/zerofs/test.txt")
+ assert "Hello from ZeroFS!" in output, f"Unexpected content: {output}"
+
+ # Test file listing
+ machine.succeed("ls -la /mnt/zerofs/")
+
+ # Test file deletion
+ machine.succeed("rm /mnt/zerofs/test.txt")
+ machine.succeed("test ! -f /mnt/zerofs/test.txt")
+
+ machine.succeed("umount /mnt/zerofs")
+ '';
+}
diff --git a/nix/nixos/zerofs.nix b/nix/nixos/zerofs.nix
new file mode 100644
index 00000000..b937723e
--- /dev/null
+++ b/nix/nixos/zerofs.nix
@@ -0,0 +1,285 @@
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.services.zerofs;
+ settingsFormat = pkgs.formats.toml { };
+
+ # Collect unix socket directories that need to be created
+ socketDirs = lib.unique (lib.filter (x: x != null) [
+ (if cfg.settings.servers.ninep != null then lib.mapNullable dirOf cfg.settings.servers.ninep.unix_socket else null)
+ (if cfg.settings.servers.nbd != null then lib.mapNullable dirOf cfg.settings.servers.nbd.unix_socket else null)
+ (if cfg.settings.servers.rpc != null then lib.mapNullable dirOf cfg.settings.servers.rpc.unix_socket else null)
+ ]);
+
+ # Filter out null values from the settings
+ filterNulls =
+ attrs:
+ lib.filterAttrsRecursive (_: v: v != null) (
+ lib.mapAttrs (
+ _: v:
+ if lib.isAttrs v then filterNulls v else v
+ ) attrs
+ );
+
+ # Build the configuration from module options
+ configFile = settingsFormat.generate "zerofs.toml" (filterNulls cfg.settings);
+
+in {
+ options.services.zerofs = {
+ enable = lib.mkEnableOption "ZeroFS, a filesystem that makes S3 your primary storage";
+
+ package = lib.mkPackageOption pkgs "zerofs" { };
+
+ readOnly = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Run ZeroFS in read-only mode. Read-only instances automatically see
+ updates from the writer and return EROFS errors for write operations.
+ '';
+ };
+
+ environmentFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = ''
+ Environment file to load before starting ZeroFS. Useful for secrets
+ like encryption passwords and cloud credentials that can be referenced
+ in the config using `''${VAR}` syntax.
+ '';
+ };
+
+ settings = lib.mkOption {
+ type = lib.types.submodule {
+ freeformType = settingsFormat.type;
+
+ options = {
+ cache = {
+ dir = lib.mkOption {
+ type = lib.types.path;
+ default = "/var/cache/zerofs";
+ description = "Directory for caching data.";
+ };
+
+ disk_size_gb = lib.mkOption {
+ type = lib.types.numbers.positive;
+ default = 10.0;
+ description = "Maximum disk cache size in GB.";
+ };
+
+ memory_size_gb = lib.mkOption {
+ type = lib.types.nullOr lib.types.numbers.positive;
+ default = null;
+ description = "Memory cache size in GB.";
+ };
+ };
+
+ storage = {
+ url = lib.mkOption {
+ type = lib.types.str;
+ example = "s3://my-bucket/zerofs-data";
+ description = ''
+ Storage backend URL. Supports:
+ - S3: `s3://bucket/path`
+ - Azure: `azure://container/path`
+ - GCS: `gs://bucket/path`
+ - Local: `file:///path/to/storage`
+ '';
+ };
+
+ encryption_password = lib.mkOption {
+ type = lib.types.str;
+ example = "\${ZEROFS_PASSWORD}";
+ description = ''
+ Encryption password for data at rest. Use environment variable
+ substitution (e.g., `''${ZEROFS_PASSWORD}`) to avoid storing
+ secrets in the Nix store.
+ '';
+ };
+ };
+
+ filesystem = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options = {
+ max_size_gb = lib.mkOption {
+ type = lib.types.nullOr lib.types.numbers.positive;
+ default = null;
+ description = ''
+ Filesystem size limit in GB. When reached, writes return ENOSPC.
+ If not specified, defaults to 16 EiB (effectively unlimited).
+ '';
+ };
+
+ compression = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ example = "zstd-3";
+ description = ''
+ Compression algorithm. Options:
+ - `lz4` (default): Fast compression
+ - `zstd-{1-22}`: Zstandard with configurable level
+ '';
+ };
+ };
+ });
+ default = null;
+ description = "Filesystem options.";
+ };
+
+ servers = lib.mkOption {
+ type = lib.types.submodule {
+ options = {
+ nfs = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options.addresses = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "127.0.0.1:2049" ];
+ description = "NFS server bind addresses.";
+ };
+ });
+ default = null;
+ description = "NFS server configuration.";
+ };
+
+ ninep = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options = {
+ addresses = lib.mkOption {
+ type = lib.types.nullOr (lib.types.listOf lib.types.str);
+ default = null;
+ description = "9P server bind addresses.";
+ };
+
+ unix_socket = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Unix socket path for 9P server.";
+ };
+ };
+ });
+ default = null;
+ description = "9P server configuration.";
+ };
+
+ nbd = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options = {
+ addresses = lib.mkOption {
+ type = lib.types.nullOr (lib.types.listOf lib.types.str);
+ default = null;
+ description = "NBD server bind addresses.";
+ };
+
+ unix_socket = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Unix socket path for NBD server.";
+ };
+ };
+ });
+ default = null;
+ description = "NBD server configuration.";
+ };
+
+ rpc = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options = {
+ addresses = lib.mkOption {
+ type = lib.types.nullOr (lib.types.listOf lib.types.str);
+ default = null;
+ description = "RPC server bind addresses.";
+ };
+
+ unix_socket = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Unix socket path for RPC server.";
+ };
+ };
+ });
+ default = null;
+ description = "RPC server configuration for admin commands.";
+ };
+ };
+ };
+ default = { };
+ description = "Server configurations for NFS, 9P, NBD, and RPC.";
+ };
+
+ lsm = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule {
+ options = {
+ l0_max_ssts = lib.mkOption {
+ type = lib.types.nullOr lib.types.ints.positive;
+ default = null;
+ description = "Max SST files in L0 before compaction (default: 16, min: 4).";
+ };
+
+ max_unflushed_gb = lib.mkOption {
+ type = lib.types.nullOr lib.types.numbers.positive;
+ default = null;
+ description = "Max unflushed data before forcing flush in GB (default: 1.0).";
+ };
+
+ max_concurrent_compactions = lib.mkOption {
+ type = lib.types.nullOr lib.types.ints.positive;
+ default = null;
+ description = "Max concurrent compaction operations (default: 8).";
+ };
+
+ flush_interval_secs = lib.mkOption {
+ type = lib.types.nullOr lib.types.ints.positive;
+ default = null;
+ description = "Interval between periodic flushes in seconds (default: 30).";
+ };
+ };
+ });
+ default = null;
+ description = "LSM tree performance tuning options.";
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Configuration for ZeroFS in TOML format. See
+ for available options.
+
+ Cloud provider credentials (aws, azure, gcp sections) can be added
+ using freeform attributes. Example:
+
+ ```nix
+ services.zerofs.settings = {
+ aws = {
+ access_key_id = "''${AWS_ACCESS_KEY_ID}";
+ secret_access_key = "''${AWS_SECRET_ACCESS_KEY}";
+ region = "us-east-1";
+ };
+ };
+ ```
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ systemd.services.zerofs = {
+ description = "ZeroFS S3 Filesystem";
+ after = [ "network-online.target" ];
+ wants = [ "network-online.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ ExecStartPre = lib.optionals (socketDirs != []) [
+ "+${pkgs.coreutils}/bin/mkdir -p ${lib.escapeShellArgs socketDirs}"
+ ];
+ ExecStart =
+ "${cfg.package}/bin/zerofs run --config ${configFile}"
+ + lib.optionalString cfg.readOnly " --read-only";
+ Restart = "always";
+ RestartSec = 5;
+ } // lib.optionalAttrs (cfg.environmentFile != null) {
+ EnvironmentFile = cfg.environmentFile;
+ };
+ };
+ };
+}
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 00000000..f9a2c6ec
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,39 @@
+{
+ lib,
+ craneLib,
+ cmake,
+ pkg-config,
+ openssl,
+}:
+
+let
+ protoFilter = path: _type: builtins.match ".*\\.proto$" path != null;
+ src = lib.cleanSourceWith {
+ src = ./..;
+ filter = path: type:
+ (protoFilter path type) || (craneLib.filterCargoSources path type);
+ };
+
+ inherit (craneLib.crateNameFromCargoToml { cargoToml = ../zerofs/Cargo.toml; }) pname version;
+
+ commonArgs = {
+ inherit src pname version;
+ cargoLock = ../zerofs/Cargo.lock;
+ # Hashes for git dependencies (required when commit is not on default branch)
+ outputHashes = {
+ "git+https://github.com/slatedb/slatedb.git?rev=22e8070316a3897f4eb5b52a3e5831a8af4485b7#22e8070316a3897f4eb5b52a3e5831a8af4485b7" = "sha256-XSA/t9AF6zC/N6i1IzPTaYJg2Fuo+3Q202tyuNDM2g8=";
+ };
+ postUnpack = ''
+ cd $sourceRoot/zerofs
+ sourceRoot="."
+ '';
+ strictDeps = true;
+ nativeBuildInputs = [ cmake pkg-config ];
+ buildInputs = [ openssl ];
+ };
+
+ cargoArtifacts = craneLib.buildDepsOnly commonArgs;
+in
+craneLib.buildPackage (commonArgs // {
+ inherit cargoArtifacts;
+})