├── LICENSE ├── README.md ├── examples ├── NUR │ ├── flake.lock │ └── flake.nix ├── httpserver │ ├── flake.lock │ └── flake.nix └── jupyter │ ├── flake.lock │ └── flake.nix ├── flake-module.nix ├── flake.nix ├── scripts ├── prepare-machine.sh └── start-container.sh └── src ├── container-up.nix └── mkContainer.nix /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Adrien Faure and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flake-containers 2 | 3 | ## Introduction 4 | A proof of concept project for defining NixOS containers (systemd-nspawn) in a flake.nix file. 5 | 6 | ## Description 7 | "flake-containers" is a small project demonstrating the usage of Nix for managing systemd containers. It utilizes the Nix language and the Nix flake system to define and launch containers with systemd-nspawn. 8 | 9 | ## Features 10 | - Define systemd containers in Nix. 11 | - Generate commands to manage the containers (mostly wrappers around machinectl, instead of the ups commands). 12 | 13 | ## Why? 14 | As stated, this is a proof of concept. I created it for experimenting with systemd-nspawn, NixOS, and flake-parts. 15 | 16 | That being said, in my development workflow, I almost always define my development dependencies into a flake for Rust, Python, Go, etc. When I need to use different services, such as a database, I would typically rely on Docker. However, with flake-containers, I can directly benefit from NixOS services and enable and configure the services that I need, in a reproducible and shareable way. 17 | 18 | It's worth mentioning that while there already exists a way to manage NixOS containers using `nixos-container`, it integrates within a NixOS configuration. This project has been largely inspired by `nixos-containers` (in fact, most of the code comes from there). However, flake-containers enables the definition and management of NixOS containers without the need to update your system configuration. Furthermore, it should work on any Linux distribution with Nix installed. 19 | 20 | ## Limitations 21 | - It requires root privileges to start the containers. 22 | - nixpkgs can be configured with a config and overlays, even changing the nixpkgs source. However, it cannot be set on a per container basis, and it is effective for each containers. 23 | - There is a dependency on flake-roots to retrieve the path for the project, where I store the states for the containers. 24 | - I dont think that the container can be updated while alive (with with nixos-rebuild switch for instance). 25 | 26 | ## Future Works 27 | - It is not a compose-style project (at least for now); there is one command per container. No "flake-containers up" command. 28 | - No support for volumes (temporary or permanent). 29 | - Volumes can be configured now in nix, but why not adding the possibility to specify volumes in the command line ? 30 | - The network configuration is currently simple and not configurable (no support for bridges for instance). 31 | - Compatibility with other distributions is untested; it has only been tested on NixOS. 32 | - The project lacks testing. It appears to work on my computer; that's the only guarantee I can offer at the moment. 33 | - There is an ugly sleep at the start time. I need a better way to detect when a container is alive to start the network configuration. 34 | - Better nix code (add comments and types) 35 | - Create a script to clean container states directory: https://github.com/NixOS/nixpkgs/issues/63028#issuecomment-507517718 36 | - Add an ephemeral mode 37 | - see --ephemeral on systemd-npsawn ? 38 | - manually setting up tmpfs ? 39 | - Add comparison with devenv, nix2containers, nixos-compose 40 | - Create a better way to specify run commands 41 | - Create a way to easily define testscripts (same as in nixos) 42 | - Find a way to transfer ownership inside the container (so for instance, when running commands from inside the container, we don't end up with a lot of root files in the host system) 43 | 44 | 45 | ## Usage 46 | 47 | 1. Start by creating a flake with the following content: 48 | ```nix 49 | { 50 | inputs = { 51 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 52 | # This project is based on flake-parts, so you need to import it 53 | flake-parts.url = "github:hercules-ci/flake-parts"; 54 | # flake-root is a dependency that enable to find the root project for the flake 55 | # repositorty to create the states for the containers 56 | flake-root.url = "github:srid/flake-root"; 57 | # Import flake-containers 58 | flake-containers.url = "github:adfaure/flake-containers"; 59 | }; 60 | outputs = 61 | inputs@{ self, nixpkgs, flake-parts, flake-containers, flake-root, ... }: 62 | flake-parts.lib.mkFlake { inherit inputs; } { 63 | imports = 64 | [ inputs.flake-containers.flakeModule inputs.flake-root.flakeModule ]; 65 | 66 | systems = [ "x86_64-linux" ]; 67 | 68 | flake-containers = { 69 | # Enable the containers 70 | enable = true; 71 | # Define the containers as nixos modules 72 | containers = { 73 | # One container named httpsserver 74 | httpserver = { 75 | # Define volumes to be bind inside the conaiter 76 | volumes = [ "/tmp" "src:/tmp/src" ]; 77 | volumes-ro = [ "/data" ]; 78 | 79 | # The configuration is a regular nixos module 80 | configuration = { pkgs, lib, ... }: { 81 | # Network configuration. 82 | networking.useDHCP = false; 83 | networking.firewall.allowedTCPPorts = [ 80 ]; 84 | 85 | # Enable a web server. 86 | services.httpd = { 87 | enable = true; 88 | adminAddr = "morty@example.org"; 89 | }; 90 | }; 91 | }; 92 | }; 93 | }; 94 | }; 95 | } 96 | ``` 97 | 3. Use `nix flake show` to see what is available. For each container, the flake defines the command to manage. 98 | ``` 99 | > nix flake show 100 | git+file:///home/adfaure/code/flake-containers?dir=examples/httpserver 101 | ├───devShells 102 | │ └───x86_64-linux 103 | │ └───flake-containers: development environment 'nix-shell' 104 | └───packages 105 | └───x86_64-linux 106 | ├───httpserver-down: package 'httpserver-down' 107 | ├───httpserver-shell: package 'httpserver-shell' 108 | └───httpserver-up: package 'httpserver-up' 109 | ``` 110 | 2. Use `nix develop .#flake-containers` to dive into a shell containing commands to manage your containers. 111 | 3. Start the container with `sudo httpserver-up` to start the container. 112 | 4. The container should appear with the `machinectl list` command. 113 | 114 | ## Similar Projects 115 | 116 | | Similar (or related) projects | Description | 117 | |-------------------------------|-------------| 118 | | [nixos-containers](https://nixos.wiki/wiki/NixOS_Containers) | Declare and manage NixOs systemd containers. | 119 | | [extra-container](https://github.com/erikarvstedt/extra-container) | Generates systemd service files for starting containers directly from systemctl. Maintains compatibility with nixos-container. | 120 | | [nixos-compose](https://gitlab.inria.fr/nixos-compose/nixos-compose) | The goal of NixOS-Compose is to reduce the burden of setting up ephemeral distributed systems thanks to Nix functional package manager and Nixos | 121 | | [devenv](https://devenv.sh/) | Use a simple unified configuration to configure packages, processes, services, scripts, git hooks, integrations. Devenv has its own way to create containers. 122 | | [nix2Container](https://github.com/nlewo/nix2container) | An archive-less dockerTools.buildImage implementation | 123 | | [Arion](https://github.com/hercules-ci/arion) |Run docker-compose with help from Nix/NixOS. | 124 | 125 | 126 | ## Contributing 127 | Contributions are welcome! Feel free to open issues for bugs or feature requests, and submit pull requests with improvements. 128 | 129 | ## License 130 | This project is licensed under the [MIT License](LICENSE). 131 | 132 | ## References 133 | 134 | - https://gitlab.inria.fr/nixos-compose/nixos-compose 135 | - https://www.tweag.io/blog/2020-07-31-nixos-flakes/ 136 | - https://github.com/tfc/nspawn-nixos 137 | - https://blog.beardhatcode.be/2020/12/Declarative-Nixos-Containers.html 138 | - https://nixos.org/manual/nixos/stable/#sec-imperative-containers 139 | - https://github.com/NixOS/nixpkgs/blob/2456e8475ffd7363fe194505ef0488dfc89a8eb1/nixos/modules/virtualisation/containers.nix#L212 140 | - https://github.com/NixOS/nixpkgs/blob/1729a61ebf54dad1fe8c3cfeeadbad530e041169/pkgs/tools/virtualization/nixos-container/nixos-container.pl -------------------------------------------------------------------------------- /examples/NUR/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-containers": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-nBipXi7TIX6HyKqWmgRPq49GA2/qBpECXw8eilBFtOY=", 7 | "path": "/nix/store/85hlh72ynixdiigrzpgrhgl9mxk7glcp-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "path": "/nix/store/85hlh72ynixdiigrzpgrhgl9mxk7glcp-source", 12 | "type": "path" 13 | } 14 | }, 15 | "flake-parts": { 16 | "inputs": { 17 | "nixpkgs-lib": "nixpkgs-lib" 18 | }, 19 | "locked": { 20 | "lastModified": 1712014858, 21 | "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", 22 | "owner": "hercules-ci", 23 | "repo": "flake-parts", 24 | "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "type": "github" 31 | } 32 | }, 33 | "flake-root": { 34 | "locked": { 35 | "lastModified": 1692742795, 36 | "narHash": "sha256-f+Y0YhVCIJ06LemO+3Xx00lIcqQxSKJHXT/yk1RTKxw=", 37 | "owner": "srid", 38 | "repo": "flake-root", 39 | "rev": "d9a70d9c7a5fd7f3258ccf48da9335e9b47c3937", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "srid", 44 | "repo": "flake-root", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs": { 49 | "locked": { 50 | "lastModified": 1712310679, 51 | "narHash": "sha256-XgC/a/giEeNkhme/AV1ToipoZ/IVm1MV2ntiK4Tm+pw=", 52 | "owner": "NixOS", 53 | "repo": "nixpkgs", 54 | "rev": "72da83d9515b43550436891f538ff41d68eecc7f", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "NixOS", 59 | "ref": "nixos-23.11", 60 | "repo": "nixpkgs", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs-lib": { 65 | "locked": { 66 | "dir": "lib", 67 | "lastModified": 1711703276, 68 | "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", 69 | "owner": "NixOS", 70 | "repo": "nixpkgs", 71 | "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "dir": "lib", 76 | "owner": "NixOS", 77 | "ref": "nixos-unstable", 78 | "repo": "nixpkgs", 79 | "type": "github" 80 | } 81 | }, 82 | "nur": { 83 | "locked": { 84 | "lastModified": 1712418268, 85 | "narHash": "sha256-ada/cxhkwk0D7/iuklXUv/EOx7ooYIn27LYAyYuoQ3o=", 86 | "owner": "nix-community", 87 | "repo": "NUR", 88 | "rev": "ade3664ee297f453ea7f31945af6b751cf800b84", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "nix-community", 93 | "repo": "NUR", 94 | "type": "github" 95 | } 96 | }, 97 | "root": { 98 | "inputs": { 99 | "flake-containers": "flake-containers", 100 | "flake-parts": "flake-parts", 101 | "flake-root": "flake-root", 102 | "nixpkgs": "nixpkgs", 103 | "nur": "nur" 104 | } 105 | } 106 | }, 107 | "root": "root", 108 | "version": 7 109 | } 110 | -------------------------------------------------------------------------------- /examples/NUR/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 4 | # Import NUR repository 5 | nur.url = "github:nix-community/NUR"; 6 | # This project is based on flake-parts, so you need to import it 7 | flake-parts.url = "github:hercules-ci/flake-parts"; 8 | # flake-root is a dependency that enable to find the root project for the flake 9 | # repositorty to create the states for the containers 10 | flake-root.url = "github:srid/flake-root"; 11 | # Import flake-containers 12 | flake-containers.url = "../.."; 13 | }; 14 | outputs = 15 | inputs@{ self, nur, nixpkgs, flake-parts, flake-containers, flake-root, ... }: 16 | flake-parts.lib.mkFlake { inherit inputs; } { 17 | imports = 18 | [ inputs.flake-containers.flakeModule inputs.flake-root.flakeModule ]; 19 | 20 | systems = [ "x86_64-linux" ]; 21 | 22 | flake-containers = { 23 | # Enable the containers 24 | enable = true; 25 | 26 | # Define and configure nixpgs 27 | nixpkgs = { 28 | overlays = [ nur.overlay ]; 29 | config.allowBroken = true; 30 | }; 31 | 32 | # Define the containers as nixos modules 33 | containers = { 34 | # One container named httpsserver 35 | nur = { 36 | configuration = { pkgs, lib, ... }: { 37 | environment.systemPackages = with pkgs; [ 38 | pkgs.nur.repos.caarlos0.timer 39 | ]; 40 | 41 | }; 42 | }; 43 | }; 44 | }; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /examples/httpserver/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-containers": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-TiB3EjSCqS/MjmQ7qf2PGWxvi0Hvi5yqSElFI6D9mgg=", 7 | "path": "/nix/store/gdgi32nr6rb1xx3f8mflwsv9j5rn1330-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "path": "/nix/store/gdgi32nr6rb1xx3f8mflwsv9j5rn1330-source", 12 | "type": "path" 13 | } 14 | }, 15 | "flake-parts": { 16 | "inputs": { 17 | "nixpkgs-lib": "nixpkgs-lib" 18 | }, 19 | "locked": { 20 | "lastModified": 1712014858, 21 | "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", 22 | "owner": "hercules-ci", 23 | "repo": "flake-parts", 24 | "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "type": "github" 31 | } 32 | }, 33 | "flake-root": { 34 | "locked": { 35 | "lastModified": 1692742795, 36 | "narHash": "sha256-f+Y0YhVCIJ06LemO+3Xx00lIcqQxSKJHXT/yk1RTKxw=", 37 | "owner": "srid", 38 | "repo": "flake-root", 39 | "rev": "d9a70d9c7a5fd7f3258ccf48da9335e9b47c3937", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "srid", 44 | "repo": "flake-root", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs": { 49 | "locked": { 50 | "lastModified": 1711668574, 51 | "narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=", 52 | "owner": "NixOS", 53 | "repo": "nixpkgs", 54 | "rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "NixOS", 59 | "ref": "nixos-23.11", 60 | "repo": "nixpkgs", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs-lib": { 65 | "locked": { 66 | "dir": "lib", 67 | "lastModified": 1711703276, 68 | "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", 69 | "owner": "NixOS", 70 | "repo": "nixpkgs", 71 | "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "dir": "lib", 76 | "owner": "NixOS", 77 | "ref": "nixos-unstable", 78 | "repo": "nixpkgs", 79 | "type": "github" 80 | } 81 | }, 82 | "root": { 83 | "inputs": { 84 | "flake-containers": "flake-containers", 85 | "flake-parts": "flake-parts", 86 | "flake-root": "flake-root", 87 | "nixpkgs": "nixpkgs" 88 | } 89 | } 90 | }, 91 | "root": "root", 92 | "version": 7 93 | } 94 | -------------------------------------------------------------------------------- /examples/httpserver/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flake containers with configured overlay"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 5 | # This project is based on flake-parts, so you need to import it 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | # flake-root is a dependency that enable to find the root project for the flake 8 | # repositorty to create the states for the containers 9 | flake-root.url = "github:srid/flake-root"; 10 | # Import flake-containers 11 | flake-containers.url = "../.."; 12 | }; 13 | outputs = 14 | inputs@{ self, nixpkgs, flake-parts, flake-containers, flake-root, ... }: 15 | flake-parts.lib.mkFlake { inherit inputs; } { 16 | imports = 17 | [ inputs.flake-containers.flakeModule inputs.flake-root.flakeModule ]; 18 | 19 | systems = [ "x86_64-linux" ]; 20 | 21 | flake-containers = { 22 | # Enable the containers 23 | enable = true; 24 | # Define the containers as nixos modules 25 | containers = { 26 | # One container named httpsserver 27 | httpserver = { 28 | # volumes = [ "/tmp" ]; 29 | # volumes-ro = [ "/tmp" ]; 30 | 31 | configuration = { pkgs, lib, ... }: { 32 | # Network configuration. 33 | networking.useDHCP = false; 34 | networking.firewall.allowedTCPPorts = [ 80 ]; 35 | 36 | # Enable a web server. 37 | services.httpd = { 38 | enable = true; 39 | adminAddr = "morty@example.org"; 40 | }; 41 | 42 | }; 43 | }; 44 | }; 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /examples/jupyter/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-containers": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-YVJCqRyNTrK1IjZdbG+a7SL+S3P8RAMJ/B7iKpwPN68=", 7 | "path": "/nix/store/b0vyal5vga3ldi360s5ww6bjk9zysmzz-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "path": "/nix/store/b0vyal5vga3ldi360s5ww6bjk9zysmzz-source", 12 | "type": "path" 13 | } 14 | }, 15 | "flake-parts": { 16 | "inputs": { 17 | "nixpkgs-lib": "nixpkgs-lib" 18 | }, 19 | "locked": { 20 | "lastModified": 1712014858, 21 | "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", 22 | "owner": "hercules-ci", 23 | "repo": "flake-parts", 24 | "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "type": "github" 31 | } 32 | }, 33 | "flake-root": { 34 | "locked": { 35 | "lastModified": 1692742795, 36 | "narHash": "sha256-f+Y0YhVCIJ06LemO+3Xx00lIcqQxSKJHXT/yk1RTKxw=", 37 | "owner": "srid", 38 | "repo": "flake-root", 39 | "rev": "d9a70d9c7a5fd7f3258ccf48da9335e9b47c3937", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "srid", 44 | "repo": "flake-root", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs": { 49 | "locked": { 50 | "lastModified": 1712437997, 51 | "narHash": "sha256-g0whLLwRvgO2FsyhY8fNk+TWenS3jg5UdlWL4uqgFeo=", 52 | "owner": "NixOS", 53 | "repo": "nixpkgs", 54 | "rev": "e38d7cb66ea4f7a0eb6681920615dfcc30fc2920", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "NixOS", 59 | "ref": "nixos-23.11", 60 | "repo": "nixpkgs", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs-lib": { 65 | "locked": { 66 | "dir": "lib", 67 | "lastModified": 1711703276, 68 | "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", 69 | "owner": "NixOS", 70 | "repo": "nixpkgs", 71 | "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "dir": "lib", 76 | "owner": "NixOS", 77 | "ref": "nixos-unstable", 78 | "repo": "nixpkgs", 79 | "type": "github" 80 | } 81 | }, 82 | "root": { 83 | "inputs": { 84 | "flake-containers": "flake-containers", 85 | "flake-parts": "flake-parts", 86 | "flake-root": "flake-root", 87 | "nixpkgs": "nixpkgs" 88 | } 89 | } 90 | }, 91 | "root": "root", 92 | "version": 7 93 | } 94 | -------------------------------------------------------------------------------- /examples/jupyter/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flake containers with configured overlay"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 5 | # This project is based on flake-parts, so you need to import it 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | # flake-root is a dependency that enable to find the root project for the flake 8 | # repositorty to create the states for the containers 9 | flake-root.url = "github:srid/flake-root"; 10 | # Import flake-containers 11 | flake-containers.url = "../.."; 12 | }; 13 | outputs = 14 | inputs@{ self, nixpkgs, flake-parts, flake-containers, flake-root, ... }: 15 | flake-parts.lib.mkFlake { inherit inputs; } { 16 | imports = 17 | [ inputs.flake-containers.flakeModule inputs.flake-root.flakeModule ]; 18 | 19 | systems = [ "x86_64-linux" ]; 20 | 21 | perSystem = { config, self', inputs', pkgs, system, ... }: { 22 | devShells.default = with pkgs; 23 | mkShell rec { 24 | buildInputs = [ 25 | (pkgs.python3.withPackages 26 | (python-pkgs: [ python-pkgs.notebook ])) 27 | ]; 28 | }; 29 | }; 30 | 31 | flake-containers = { 32 | # Enable the containers 33 | enable = true; 34 | # Define the containers as nixos modules 35 | containers = let srcdir = "/tmp/src"; 36 | in { 37 | jupyter = { 38 | # 39 | volumes = [ ".:${srcdir}" ]; 40 | 41 | # How to create a better way to define runCommand ? 42 | runCommand = pkgs: 43 | let 44 | pythonEnv = (pkgs.python3.withPackages 45 | (python-pkgs: [ python-pkgs.notebook ])); 46 | in '' 47 | cd ${srcdir} ; ${pythonEnv}/bin/python -m jupyter notebook --ip 0.0.0.0 --allow-root 48 | ''; 49 | 50 | configuration = { pkgs, lib, ... }: { 51 | # Network configuration. 52 | networking.useDHCP = false; 53 | networking.firewall.allowedTCPPorts = [ 8888 ]; 54 | }; 55 | }; 56 | }; 57 | }; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /flake-module.nix: -------------------------------------------------------------------------------- 1 | toplevel@{ pkgs, config, inputs, lib, withSystem, ... }: 2 | let 3 | inherit (builtins) listToAttrs attrNames attrValues foldl' length filter; 4 | inherit (lib) 5 | mkIf mkOption mkDefault mkMerge mapAttrs mkEnableOption types 6 | recursiveUpdateUntil isDerivation literalExpression; 7 | 8 | cfg = config.flake-containers; 9 | 10 | overlayType = types.uniq 11 | (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified))); 12 | 13 | containerOptionType = types.submodule { 14 | options = { 15 | configuration = mkOption {}; 16 | volumes-ro = mkOption { 17 | default = []; 18 | }; 19 | volumes = mkOption { 20 | default = []; 21 | }; 22 | runCommand = mkOption { 23 | default = null; 24 | }; 25 | }; 26 | }; 27 | 28 | nixpkgsOptionType = types.submodule { 29 | options = { 30 | nixpkgs = mkOption { 31 | type = types.path; 32 | default = inputs.nixpkgs; 33 | defaultText = literalExpression "inputs.nixpkgs"; 34 | description = '' 35 | The nixpkgs flake to use. 36 | 37 | This option needs to set if the nixpkgs that you want to use is under a different name 38 | in flake inputs. 39 | ''; 40 | }; 41 | config = mkOption { 42 | default = { }; 43 | type = types.attrs; 44 | description = '' 45 | The configuration of the Nix Packages collection. 46 | ''; 47 | example = literalExpression '' 48 | { allowUnfree = true; } 49 | ''; 50 | }; 51 | overlays = mkOption { 52 | default = [ ]; 53 | type = types.uniq (types.listOf overlayType); 54 | description = '' 55 | List of overlays to use with the Nix Packages collection. 56 | ''; 57 | example = literalExpression '' 58 | [ 59 | inputs.fenix.overlays.default 60 | ] 61 | ''; 62 | }; 63 | }; 64 | }; 65 | 66 | flakeContainersConfigType = types.submodule { 67 | options = { 68 | enable = mkEnableOption "flake containers"; 69 | nixpkgs = mkOption { 70 | type = nixpkgsOptionType; 71 | default = { }; 72 | description = '' 73 | Config about the nixpkgs used by flake containers. 74 | ''; 75 | }; 76 | containers = mkOption { 77 | type = types.attrsOf containerOptionType; 78 | default = { }; 79 | description = '' 80 | Container configuration. Defines the system modules, volumes etc 81 | ''; 82 | }; 83 | }; 84 | }; 85 | 86 | in { 87 | options.flake-containers = mkOption { 88 | type = flakeContainersConfigType; 89 | default = { }; 90 | description = '' 91 | The config for flake-containers. 92 | ''; 93 | }; 94 | config = mkIf cfg.enable { 95 | perSystem = perSystemScope@{ config, self', lib, pkgs, system, ... }: 96 | let 97 | mergeIntoSet = lib.foldr (a: b: a // b) { }; 98 | 99 | # Fot the moment, map containers to private adresses 100 | allocatedAdresses = mergeIntoSet (lib.imap1 (i: name: { 101 | "${name}" = let ipPrefix = "10.233.${builtins.toString i}"; 102 | in { 103 | hostAddress = "${ipPrefix}.1"; 104 | localAddress = "${ipPrefix}.2"; 105 | }; 106 | }) ((lib.mapAttrsToList (name: config: name)) cfg.containers)); 107 | 108 | # Create a the commands that manage the containers 109 | mkContainer = 110 | import ./src/mkContainer.nix { inherit lib pkgs perSystemScope; }; 111 | 112 | # Create all containers 113 | containers = lib.mapAttrsToList (name: container: 114 | mkContainer name container allocatedAdresses."${name}".localAddress 115 | allocatedAdresses."${name}".hostAddress) cfg.containers; 116 | 117 | # Create a list to be add to the buildInputs 118 | to-list = lib.concatMap 119 | # Create a list from the set 120 | (lib.mapAttrsToList (name: command: command)) 121 | # Get the commands attribute for each container 122 | (lib.forEach containers (container: container.commands)); 123 | 124 | to-attribute-set = mergeIntoSet 125 | # Get the commands attribute for each container 126 | (lib.forEach containers (container: container.commands)); 127 | 128 | selectedPkgs = import cfg.nixpkgs.nixpkgs { 129 | inherit system; 130 | overlays = cfg.nixpkgs.overlays; 131 | config = cfg.nixpkgs.config; 132 | }; 133 | 134 | shellHookHelpStr = let 135 | commands-list = lib.foldr (a: b: a + "\n" + b) " " (lib.flatten 136 | (lib.forEach containers 137 | (container: builtins.attrNames container.commands))); 138 | in '' 139 | flake-containers Shell Hook! 140 | 141 | The following commands are available (sudo is required for the up commands): 142 | ${commands-list} 143 | ''; 144 | 145 | in { 146 | # flake-part way to specify nixpkgs 147 | _module.args.pkgs = selectedPkgs; 148 | 149 | # Create packages so they can be used directly from nix run 150 | packages = to-attribute-set; 151 | 152 | # Empty for the example 153 | devShells.flake-containers = pkgs.mkShell { 154 | shellHook = ''echo "${shellHookHelpStr}"''; 155 | nativeBuildInputs = lib.flatten to-list; 156 | }; 157 | }; 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A flake module to compose nixos systemd-nspawn containers."; 3 | outputs = { self, ... }: { 4 | flakeModules.default = ./flake-module.nix; 5 | flakeModule = self.flakeModules.default; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /scripts/prepare-machine.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Name of the container 4 | machine=$1 5 | # Top level 6 | toplevel=$2 7 | 8 | base_dir="/tmp/nc-tests" 9 | 10 | if [[ -z "$machine" ]]; then 11 | echo "machine name required" 12 | exit 1 13 | fi 14 | if [[ -z "$toplevel" ]]; then 15 | echo "toplevel path required" 16 | exit 1 17 | fi 18 | machine_dir="$base_dir/$machine" 19 | 20 | if [ -d $machine_dir ]; then 21 | echo "$machine_dir already exists" 22 | exit 1 23 | fi 24 | 25 | mkdir -p $machine_dir 26 | cd $machine_dir 27 | 28 | # mkdir dev etc nix proc sbin sys 29 | #mkdir -p dev etc nix/store proc sbin sys run/wrappers home bin root usr var 30 | # dev proc sys 31 | mkdir -p etc nix/store sbin home bin root usr var/lib run tmp 32 | 33 | mkdir -p etc/nxc 34 | echo $machine > etc/nxc/hostname 35 | 36 | ln -s $toplevel/etc/os-release etc/ 37 | # ln -s $toplevel/init sbin/ 38 | 39 | ln -s $toplevel/sw/bin/sh bin/sh 40 | 41 | # mount --bind -o ro /nix/store $machine_dir/nix/store 42 | 43 | # mount -t tmpfs tmpfs $machine_dir/run 44 | # mkdir -p $machine_dir/run/wrappers 45 | # mount -t tmpfs -o exec,suid tmpfs $machine_dir/run/wrappers 46 | # mount -t tmpfs -o exec,mode=777 tmpfs $machine_dir/tmp 47 | 48 | # set -ex 49 | # 50 | # root="/tmp/nc-test" 51 | # 52 | # mkdir -p -m 0755 "$root/etc" "$root/var/lib" 53 | # mkdir -p -m 0700 "$root/var/lib/private" "$root/root" # /run/containers 54 | # 55 | # if ! [ -e "$root/etc/os-release" ]; then 56 | # touch "$root/etc/os-release" 57 | # fi 58 | # 59 | # if ! [ -e "$root/etc/machine-id" ]; then 60 | # touch "$root/etc/machine-id" 61 | # fi 62 | # 63 | # mkdir -p -m 0755 \ 64 | # "/nix/var/nix/profiles/per-container/$INSTANCE" \ 65 | # "/nix/var/nix/gcroots/per-container/$INSTANCE" 66 | # 67 | # cp --remove-destination /etc/resolv.conf "$root/etc/resolv.conf" 68 | # 69 | # if [ "$PRIVATE_NETWORK" = 1 ]; then 70 | # extraFlags+=" --network-veth" 71 | # if [ -n "$HOST_BRIDGE" ]; then 72 | # extraFlags+=" --network-bridge=$HOST_BRIDGE" 73 | # fi 74 | # if [ -n "$HOST_PORT" ]; then 75 | # OIFS=$IFS 76 | # IFS="," 77 | # for i in $HOST_PORT 78 | # do 79 | # extraFlags+=" --port=$i" 80 | # done 81 | # IFS=$OIFS 82 | # fi 83 | # fi 84 | # 85 | # # extraFlags+=" ${concatStringsSep " " (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)}" 86 | # 87 | # for iface in $INTERFACES; do 88 | # extraFlags+=" --network-interface=$iface" 89 | # done 90 | # 91 | # for iface in $MACVLANS; do 92 | # extraFlags+=" --network-macvlan=$iface" 93 | # done 94 | # 95 | # # If the host is 64-bit and the container is 32-bit, add a 96 | # # --personality flag. 97 | # ${optionalString (config.nixpkgs.system == "x86_64-linux") '' 98 | # if [ "$(< ''${SYSTEM_PATH:-/nix/var/nix/profiles/per-container/$INSTANCE/system}/system)" = i686-linux ]; then 99 | # extraFlags+=" --personality=x86" 100 | # fi 101 | # ''} 102 | 103 | # # Run systemd-nspawn without startup notification (we'll 104 | # # wait for the container systemd to signal readiness). 105 | # exec ${config.systemd.package}/bin/systemd-nspawn \ 106 | # --keep-unit \ 107 | # -M "$INSTANCE" -D "$root" $extraFlags \ 108 | # $EXTRA_NSPAWN_FLAGS \ 109 | # --notify-ready=yes \ 110 | # --bind-ro=/nix/store \ 111 | # --bind-ro=/nix/var/nix/db \ 112 | # --bind-ro=/nix/var/nix/daemon-socket \ 113 | # --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \ 114 | # --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \ 115 | # --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \ 116 | # --setenv HOST_BRIDGE="$HOST_BRIDGE" \ 117 | # --setenv HOST_ADDRESS="$HOST_ADDRESS" \ 118 | # --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \ 119 | # --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \ 120 | # --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \ 121 | # --setenv HOST_PORT="$HOST_PORT" \ 122 | # --setenv PATH="$PATH" \ 123 | # ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then 124 | # ''--capability="${concatStringsSep " " cfg.additionalCapabilities}"'' else "" 125 | # } \ 126 | # ${if cfg.tmpfs != null && cfg.tmpfs != [] then 127 | # ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else "" 128 | # } \ 129 | # ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init" 130 | -------------------------------------------------------------------------------- /scripts/start-container.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | base_dir="/tmp/nc-tests" 5 | machine="nc-test" 6 | machine_dir="$base_dir/$machine" 7 | 8 | toplevel="/nix/store/ach3867rm8y3156ck238qrsrgyy6ck88-nixos-system-nixos-23.11pre-git" 9 | 10 | exec systemd-nspawn \ 11 | -M "$machine" -D "$machine_dir" \ 12 | -U \ 13 | --notify-ready=yes \ 14 | --kill-signal=SIGRTMIN+3 \ 15 | --bind-ro=/nix/store \ 16 | --bind-ro=/nix/var/nix/db \ 17 | --bind-ro=/nix/var/nix/daemon-socket \ 18 | --bind="$toplvel:/nix/var/nix/profiles" \ 19 | --bind="$toplevel:/nix/var/nix/gcroots" \ 20 | --link-journal=try-guest \ 21 | "$toplevel/init" -------------------------------------------------------------------------------- /src/container-up.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, rootpath, name, flake-root, localAddress, hostAddress, volumes, volumes-ro, containerConfig, ... }: 2 | # Files that contains the code to start a container. 3 | # The original works come from nixos-containers services (https://github.com/NixOS/nixpkgs/blob/8b152a2242d4f29de1c072f833ab941dd141c510/nixos/modules/virtualisation/nixos-containers.nix#L43) 4 | # That is used to define and manage systemd-containers within a nixos configuration. 5 | let 6 | inherit (lib) getExe optionalString mapAttrsToList concatStringsSep; 7 | 8 | flakeContainersBaseDir = ".flake-containers"; 9 | 10 | # Base container volume, that will be the / of the container 11 | containerSystemDir = "${flakeContainersBaseDir}/${name}/config"; 12 | 13 | # Contains container configuration to configure its launching parameters (volumes, network etc) 14 | containerConfigDir = "${flakeContainersBaseDir}/${name}/volume"; 15 | 16 | extraVolumes = (builtins.concatStringsSep " " (lib.forEach volumes (volume: 17 | ''--bind="${volume}"'' 18 | ))); 19 | 20 | extraRoVolumes = (builtins.concatStringsSep " " (lib.forEach volumes-ro (volume: 21 | ''--bind-ro="${volume}"'' 22 | ))); 23 | 24 | initCommand = if containerConfig.runCommand != null then 25 | let 26 | wrappedCommand = pkgs.writeScript "container-init" '' 27 | ${containerConfig.runCommand pkgs} 28 | ''; 29 | in "${wrappedCommand}" 30 | else 31 | "${rootpath}/init"; 32 | 33 | containerInit = pkgs.writeScript "container-init" '' 34 | #! ${pkgs.runtimeShell} -e 35 | 36 | # Exit early if we're asked to shut down. 37 | trap "exit 0" SIGRTMIN+3 38 | 39 | ${pkgs.iproute2}/bin/ip link set host0 name eth0 40 | ${pkgs.iproute2}/bin/ip link set dev eth0 up 41 | 42 | ${pkgs.iproute2}/bin/ip addr add ${localAddress} dev eth0 43 | 44 | ${pkgs.iproute2}/bin/ip route add ${hostAddress} dev eth0 45 | ${pkgs.iproute2}/bin/ip route add default via ${hostAddress} 46 | 47 | # Start the regular stage 2 script. 48 | # We source instead of exec to not lose an early stop signal, which is 49 | # also the only _reliable_ shutdown signal we have since early stop 50 | # does not execute ExecStop* commands. 51 | set +e 52 | . "$1" 53 | ''; 54 | 55 | ipcall = ipcmd: variable: '' 56 | ${ipcmd} add ${variable} dev $ifaceHost 57 | ''; 58 | 59 | containerUpScript = pkgs.writeShellApplication { 60 | name = "${name}-up"; 61 | text = '' 62 | # TODO: ensure execution 63 | trap 'chattr -i ''${machine_dir}/var/empty' EXIT SIGINT SIGTERM 64 | 65 | # TODO: see how they get rootpath 66 | # https://github.com/Platonic-Systems/mission-control/blob/master/nix/flake-module.nix#L76C32-L76C67 67 | 68 | # Tricks taken from https://github.com/Platonic-Systems/mission-control/blob/a562943f45d9b8ae63dd62ec084202fdbdbeb83f/nix/wrapper.nix#L45 69 | # It allow me to get the flake folder as the base dir for the containers. 70 | # but it also create a dependency on flake-root... 71 | FLAKE_ROOT="$(${lib.getExe flake-root})" 72 | 73 | machine_dir="$FLAKE_ROOT/${containerSystemDir}" 74 | 75 | if [ -d "$machine_dir" ]; then 76 | echo "$machine_dir already exists" 77 | # exit 1 78 | fi 79 | 80 | command_dir=$PWD 81 | 82 | mkdir -p "$machine_dir" 83 | cd "$machine_dir" 84 | 85 | mkdir -p etc nix/store sbin home bin root usr var/lib run tmp 86 | mkdir -p etc/nxcconcatStringsSep 87 | # Force for the moment 88 | ln -snf ${rootpath}/etc/os-release etc/ 89 | ln -snf ${rootpath}/sw/bin/sh bin/sh 90 | 91 | # Clean previous interfaces 92 | ip link del dev "ve-${name}" 2> /dev/null || true 93 | ip link del dev "vb-${name}" 2> /dev/null || true 94 | 95 | # Get back to where the command was launched 96 | cd "$command_dir" 97 | 98 | # start container in subprocess 99 | systemd-nspawn \ 100 | -M "${name}" -D "$machine_dir" \ 101 | --private-network \ 102 | --network-veth \ 103 | --notify-ready=yes \ 104 | --kill-signal=SIGRTMIN+3 \ 105 | --bind-ro=/nix/store \ 106 | --bind-ro=/nix/var/nix/db \ 107 | --bind-ro=/nix/var/nix/daemon-socket \ 108 | --bind="${rootpath}:/nix/var/nix/profiles" \ 109 | --bind="${rootpath}:/nix/var/nix/gcroots" \ 110 | --link-journal=try-guest \ 111 | ${extraVolumes} \ 112 | ${extraRoVolumes} \ 113 | --capability="CAP_NET_ADMIN" \ 114 | ${containerInit} "${initCommand}" & 115 | 116 | # FIXME: Aouch 117 | sleep 5s 118 | 119 | # Activate the network 120 | ip link set dev ve-${name} up 121 | 122 | ${pkgs.iproute2}/bin/ip addr add ${hostAddress} dev ve-${name} 123 | ${pkgs.iproute2}/bin/ip route add ${localAddress} dev ve-${name} 124 | 125 | # wait end of containter 126 | wait 127 | 128 | ''; 129 | }; 130 | in containerUpScript 131 | -------------------------------------------------------------------------------- /src/mkContainer.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, perSystemScope, ... }: 2 | # Create a the commands that manage the containers 3 | name: container: localAddress: hostAddress: 4 | let 5 | containerConfig = { 6 | # system.stateVersion = lib.mkDefault lib.trivial.release; 7 | imports = [ 8 | # Minimal module that declares a container 9 | ({ pkgs, lib, modulesPath, ... }: { 10 | boot.isContainer = true; 11 | # imports = [ "${modulesPath}/profiles/minimal.nix" ]; 12 | systemd.sockets.nix-daemon.enable = lib.mkDefault false; 13 | systemd.services.nix-daemon.enable = lib.mkDefault false; 14 | }) 15 | # user defined module 16 | container.configuration 17 | ]; 18 | }; 19 | container-name = name; 20 | builtContainer = pkgs.nixos containerConfig; 21 | rootPath = "${builtContainer.toplevel}"; 22 | in { 23 | inherit container-name builtContainer rootPath; 24 | commands = { 25 | # Maybe not the best way to import it 26 | # TODO: make it a function instead ? 27 | "${container-name}-up" = (import ./container-up.nix) { 28 | inherit pkgs lib localAddress hostAddress; 29 | volumes = container.volumes; 30 | volumes-ro = container.volumes-ro; 31 | rootpath = rootPath; 32 | containerConfig = container; 33 | flake-root = perSystemScope.config.flake-root.package; 34 | name = container-name; 35 | }; 36 | "${container-name}-down" = pkgs.writeShellApplication { 37 | name = "${container-name}-down"; 38 | text = '' 39 | machinectl stop ${container-name} 40 | ''; 41 | }; 42 | "${container-name}-shell" = pkgs.writeShellApplication { 43 | name = "${container-name}-shell"; 44 | text = '' 45 | machinectl shell ${container-name} 46 | ''; 47 | }; 48 | }; 49 | } 50 | --------------------------------------------------------------------------------