/extra-container

Run declarative NixOS containers without full system rebuilds (allow overriding pkgs)

Primary LanguageShellMIT LicenseMIT

extra-container

Manage declarative NixOS containers like imperative containers, without system rebuilds.

Each declarative container adds a full system module evaluation to every NixOS rebuild, which can be prohibitively slow for systems with many containers or when experimenting with single containers.

On the other hand, the faster imperative containers lack the full range of options of declarative containers. This tool brings you the best of both worlds.

Example

sudo extra-container create --start <<'EOF'
{
  containers.demo = {
    privateNetwork = true;
    hostAddress = "10.250.0.1";
    localAddress = "10.250.0.2";

    config = { pkgs, ... }: {
      systemd.services.hello = {
        wantedBy = [ "multi-user.target" ];
        script = ''
          while true; do
            echo hello | ${pkgs.netcat}/bin/nc -lN 50
          done
        '';
      };
      networking.firewall.allowedTCPPorts = [ 50 ];
    };
  };
}
EOF

curl --http0.9 10.250.0.2:50 # Returns 'hello' from the container

# Now change the 'hello' string in the container definition to something
# else and re-run the `extra-container create --start` command.
# The container is automatically updated via NixOS' `switch-to-configuration`.

# The container is a regular container that can be controlled
# with nixos-container
nixos-container status demo

# Remove the container
sudo extra-container destroy demo

Run command in a container and exit

cfg='{
  containers.demo.config = {
    networking.hostName = "hello";
  };
}'
extra-container shell -E "$cfg" --run c hostname # => hello

Changelog

CHANGELOG.md

Install

On NixOS

NixOS ≥ 21.11

Add programs.extra-container.enable = true to your configuration.

Any NixOS with flake support

Import extra-container.nixosModules.default in your configuration.

On other systemd-based Linux distros

git clone https://github.com/erikarvstedt/extra-container
# Calls sudo during install
extra-container/util/install.sh

install.sh installs extra-container to the root nix user profile and edits /etc/sudoers to enable running extra-container with sudo.

More features

Shell

Command shell starts a container shell session. The shell provides helper functions for interacting with the container. The container is destroyed when exiting the shell.

This config uses extra options that are explained below.

read -d '' src <<'EOF' || :
{
  containers.demo = {
    extra.addressPrefix = "10.250.0"; # Sets up a private network.
    extra.enableWAN = true;
  };
}
EOF
# Provide container config via `-E` instead of stdin because the shell's stdin
# should be connected to the terminal.
extra-container shell -E "$src" --ssh

extra-container automatically runs itself via sudo when called as a non-root user.

An example shell session

...
Starting shell.
Enter "h" for documentation.

$ h
Container address: 10.250.0.2 ($ip)
Container filesystem: /var/lib/nixos-containers/demo

Run "c COMMAND" to execute a command in the container
Run "c" to start a shell session inside the container
Run "cssh" for SSH

# Container internet access, enabled via option `extra.enableWAN`
$ c curl example.com
<!doctype html>
<html>
...

# Connect with SSH, enabled by `--ssh`
$ cssh hostname
demo

Run commands

Run a command in a shell session and exit. The container is destroyed afterwards.

cfg='{ containers.demo = {}; }'
extra-container shell -E "$cfg" --run c hostname
# => demo

Start a shell inside the container.

cfg='{ containers.demo = {}; }'
extra-container shell -E "$cfg" --run c

Repeated calls to extra-container shell

When extra-container shell detects that it is already running in a container shell session, it updates the running container instead of destroying and restarting it and starting a new shell.
To prevent sudo from clearing the environment variables that are needed for shell detection, call extra-container without sudo.
extra-container will automatically run itself via sudo only when it is first called as a non-root user outside of a shell session.

To force container destruction inside a shell session, use extra-container shell --destroy|-d.

Disable auto-destruction

By default, shell destroys the shell container before starting and before exiting. This ensures that containers start with no leftover filesystem state from previous runs and that containers do not consume system resources after use.
To disable auto-destructing containers, run extra-container shell --no-destroy|-n

Private network helper

Container options extra.* are defined by extra-container and help with setting up private network containers.
See eval-config.nix for full option descriptions.

containers.demo = {
  extra = {
    # Sets
    # privateNetwork = true
    # hostAddress = "${addressPrefix}.1"
    # localAddress = "${addressPrefix}.2"
    addressPrefix = "10.250.0";

    # Enable internet access for the container
    enableWAN = true;
    # Always allow connections from hostAddress
    firewallAllowHost = true;
    # Make the container's localhost reachable via localAddress
    exposeLocalhost = true;
  }
};

Access working dir in non-file configs

extra-container appends pwd to NIX_PATH to allow configs given via --expr|-E or via stdin to access the working directory.

extra-container create -E '{ imports = [ <pwd/myfile.nix> ]; ... }'

Define containers via Flakes

See examples/flake.

Usage

extra-container create <container-config-file>
                       [--attr|-A attrPath]
                       [--nixpkgs-path|--nixos-path path]
                       [--start|-s | --restart-changed|-r]
                       [--ssh]
                       [--build-args arg...]

    <container-config-file> is a NixOS config file with container
    definitions like 'containers.mycontainer = { ... }'

    --attr | -A attrPath
      Select an attribute from the config expression

    --nixpkgs-path
      A nix expression that returns a path to the nixpkgs source
      to use for building the containers

    --nixos-path
      Like '--nixpkgs-path', but for directly specifying the NixOS source

    --start | -s
      Start all created containers
      Update running containers that have changed or restart them if '--restart-changed' was specified

    --update-changed | -u
      Update running containers with a changed system configuration by running
      'switch-to-configuration' inside the container.
      Restart containers with a changed container configuration

    --restart-changed | -r
      Restart running containers that have changed

    --ssh
      Generate SSH keys in /tmp and enable container SSH access.
      The key files remain after exit and are reused on subsequent runs.
      Unlocks the function 'cssh' in 'extra-container shell'.
      Requires container option 'privateNetwork = true'.

    --build-args arg...
      All following args are passed to nix-build.

    Example:
      extra-container create mycontainers.nix --restart-changed

      extra-container create mycontainers.nix --nixpkgs-path \
        'fetchTarball https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz'

      extra-container create mycontainers.nix --start --build-args --builders 'ssh://worker - - 8'

echo <container-config> | extra-container create
    Read the container config from stdin

    Example:
      extra-container create --start <<EOF
        { containers.hello = { enableTun = true; config = {}; }; }
      EOF

extra-container create --expr|-E <container-config>
    Provide container config as an argument

extra-container create <store-path>
    Create containers from <store-path>/etc

    Examples:
      Create from nixos system derivation
      extra-container create /nix/store/9h..27-nixos-system-foo-18.03

      Create from nixos etc derivation
      extra-container create /nix/store/32..9j-etc

extra-container shell ...
    Start a container shell session.
    See the README for a complete documentation.
    Supports all arguments from 'create'

    Extra arguments:
      --run <cmd> <arg>...
        Run command in shell session and exit
        Must be the last option given
      --no-destroy|-n
        Do not destroy shell container before and after running
      --destroy|-d
        If running inside an existing shell session, force container to
        be destroyed before and after running

    Example:
      extra-container shell -E '{ containers.demo.config = {}; }'

extra-container build ...
    Build the container config and print the resulting NixOS system etc path

    This command can be used like 'create', but options related
    to starting are not supported

extra-container list
    List all extra containers

extra-container restart <container>...
    Fixes the broken restart command of nixos-container (nixpkgs issue #43652)

extra-container destroy <container-name>...
    Destroy containers

extra-container destroy <args for create/shell>...
    Destroy the containers defined by the args for command `create` or `shell` (see above).
    For this to work, the first arg after `destroy` must start with one of the
    following three characters: ./-

    Example:
      extra-container destroy ./containers.nix

extra-container destroy --all|-a
    Destroy all extra containers

extra-container <cmd> <arg>...
    All other commands are forwarded to nixos-container

Implementation

The script works like this: Take a NixOS config with container definitions and build the system's config.system.build.etc derivation. Because we're not building a full system we can use a reduced module set (eval-config.nix) to improve evaluation performance.

Now link the container files from the etc derivation to the main system, like so:

nixos-system/etc/systemd/system/container@CONTAINER.service -> /etc/systemd-mutable/system
nixos-system/etc/containers/CONTAINER.conf -> /etc/containers       (system.stateVersion < 22.05)
                                           -> /etc/nixos-containers (system.stateVersion ≥ 22.05)

Finally, add gcroots pointing to the linked files.

Developing

All contributions and suggestions are welcome, even if they're minor or cosmetic.

Development workflow

Run nix develop in the project root directory to start a development shell.
Within the shell, you can run extra-container from the local source via command extra-container.

When changing the Usage documentation in extra-container, run make doc to copy these changes to README.md.

Tests

Run make to run the tests.
The following tests are executed:

  • Main test suite

    Can be run manually via sudo run-tests-in-container.sh.
    This script creates a temporary container named test-extra-container in which the main test script test.sh is run.
    test.sh adds and removes temporary containers named test-* on the host system. It can also be called directly, but wrapping it with run-tests-in-container.sh helps reducing interference with your main system.

  • VM test

    Can be run manually via nix build .#test.
    This is a basic test using the NixOS VM test framework. It is built as a Nix derivation, which makes it independent from the system environment.

    For debugging the VM, run nix run .#debugTest to start a Python test driver shell inside the VM.

  • nix flake check

    Evaluates all flake outputs and builds the VM test.

VM

Run nix run .#vm to start a VM where extra-container is installed.
This provides an isolated and reproducible testing environment.