Overview
Container services run distribution-native root filesystems under OpenRC without Docker, Podman, containerd, or a Nix-store-based root filesystem. Deployment creates a ZFS dataset, downloads and verifies the selected base system, installs requested packages, configures namespaces and networking, applies a cgroup profile, and generates an OpenRC service.
Every container is opt-in. A declaration is inert until enable = true is set explicitly; a disabled container creates no dataset, root filesystem, network namespace, firewall rules, or OpenRC service.
The current implementation supports Alpine Linux 3.24.1 and Chimera Linux 20251220 as guest systems, both on x86_64 by default. Select one with os.
Minimal container
This is the smallest active declaration. It creates an Alpine root filesystem and runs the default idle command.
services.nftables.enable = true;
container.example = {
enable = true;
os = "alpine";
};
The generated OpenRC service is named container-example.
Disabled declaration
This configuration has no runtime or storage effect.
container.example = {
enable = false;
os = "alpine";
};
Profiles and networking
profile selects a complete baseline policy. It currently provides two things: a cgroup preset and a default network mode.
| Profile | Default network | memory.max | pids.max | cpu.weight |
|---|---|---|---|---|
default | internet | 512M | 256 | 100 |
isolated | isolated | 256M | 128 | 50 |
network controls only runtime connectivity. Its default value is null, which means that the network mode is inherited from profile. Setting network explicitly replaces the profile's network choice without changing memory, PID, or CPU limits.
The effective settings are resolved in this order:
- Select the cgroup and network defaults from
profile. - Replace the network mode when
networkis notnull. - Replace individual cgroup keys from
cgroup.settings.
| Configuration | Effective network | Effective cgroup preset |
|---|---|---|
profile = "default" | internet | default |
profile = "isolated" | isolated | isolated |
profile = "default"; network = "isolated" | isolated | default |
profile = "isolated"; network = "internet" | internet | isolated |
Use profile when choosing the general resource and isolation policy. Use network only for an intentional exception to that policy.
Internet-enabled service
This example installs Nginx inside Alpine and runs it in the foreground. The container receives outbound internet access for package installation and runtime traffic. The host must explicitly set services.nftables.enable = true; the container module does not enable the firewall automatically.
services.nftables.enable = true;
container.web = {
enable = true;
os = "alpine";
profile = "default";
packages = [ "nginx" ];
command = [ "/usr/sbin/nginx" "-g" "daemon off;" ];
};
The current networking implementation provides outbound NAT but does not declare stable host port forwarding. Do not treat the automatically assigned 10.240.* address as a public interface contract.
packages contains package names for the selected guest system, installed with its apk during deployment. command is the foreground process started by OpenRC inside the container; each argument is a separate list element.
Isolated container
This example has no Ethernet interface, route, DNS connectivity, or internet access at runtime.
container.worker = {
enable = true;
os = "alpine";
profile = "isolated";
};
Custom cgroup limits
Explicit settings override the selected profile one key at a time.
container.processor = {
enable = true;
os = "alpine";
profile = "isolated";
cgroup.settings = {
"memory.max" = "1G";
"pids.max" = 64;
"cpu.weight" = 200;
};
};
The values are written to the OpenRC service's cgroup v2 configuration.
Network modes and overrides
The internet mode creates a private network namespace containing loopback and an eth0 veth interface. The host receives the other end of the veth pair, assigns the private subnet 10.240.<networkId>.0/30 with .1 on the host and .2 in the container, installs a default route in the container, enables IPv4 forwarding, and adds nftables forwarding and masquerade rules. This provides outbound IPv4 access but no stable public address or automatic host port forwarding.
networkId defaults to a value derived from the container name, so the subnet stays the same as long as the name does. Two names can lead to the same value. Evaluation then fails with a message that internet-enabled containers must use distinct networkId values, and one of them needs an explicit value between 1 and 254:
container.example.networkId = 42;
The isolated mode creates a private network namespace containing only loopback. It creates no veth interface, default route, forwarding rule, or NAT rule. A DNS configuration file may exist in the root filesystem, but it is unusable without a network route.
Runtime networking is separate from deployment-time package installation. packages is installed by the host during activation before the service enters its runtime network namespace, so an isolated container can still receive declared packages during deployment.
This example retains the default profile's cgroup limits while disabling runtime network access:
container.local-api = {
enable = true;
os = "alpine";
profile = "default";
network = "isolated";
};
This example retains the stricter isolated cgroup limits while enabling runtime internet access:
container.restricted-fetcher = {
enable = true;
os = "alpine";
profile = "isolated";
network = "internet";
};
Valid explicit values are "internet" and "isolated". Use network = null or omit the option to inherit the profile default.
ZFS storage
On a ZFS-root host, the dataset is inferred automatically as <root-pool>/containers/<name>. For example, container.web on rpool/root uses rpool/containers/web and mounts it at /var/lib/copland-containers/web.
container.web = {
enable = true;
os = "alpine";
quota = "4G";
};
Use an explicit dataset when the host root filesystem is not on ZFS or when the container belongs in another pool.
container.archive = {
enable = true;
os = "alpine";
profile = "isolated";
dataset = "tank/containers/archive";
mountpoint = "/srv/containers/archive";
quota = "20G";
};
Dataset creation uses zfs create -p and therefore creates missing parent datasets. The root filesystem is stored under <mountpoint>/rootfs and is writable; no OverlayFS layer is used.
Set quota = null to create the dataset without a quota.
Combined settings example
container.archive-worker = {
enable = true;
os = "alpine";
profile = "isolated";
dataset = "rpool/containers/archive-worker";
mountpoint = "/var/lib/copland-containers/archive-worker";
quota = "8G";
hostname = "archive-worker";
cgroup.settings = {
"memory.max" = "768M";
"pids.max" = 96;
};
};
Base image pinning
The release, mirror, architecture, and SHA-256 digest of each guest system are pinned in the module. Override all relevant values together when intentionally selecting another base image.
container.custom = {
enable = true;
os = "alpine";
alpine = {
version = "3.24.1";
arch = "x86_64";
mirror = "https://dl-cdn.alpinelinux.org/alpine";
sha256 = "41f73e3cf5fa919b8aa5ca6b30dc48f0da2720776d7423e2a7748211456fe081";
};
};
Chimera Linux publishes its root filesystems in dated directories. chimera.version selects that date, so a new Chimera release is chosen by changing the date and its digest together:
container.chimera = {
enable = true;
os = "chimera";
chimera = {
version = "20251220";
arch = "x86_64";
sha256 = "d497b90e75ab739c7d60bd082a3e0f68c2a63f7ee472005a8f9f2a42ba367be7";
};
};
Deployment refuses to replace an existing dataset with a different base image, including a change of os. Use a new dataset or perform an explicit migration instead of silently replacing writable container state.
Deployment lifecycle
During activation, zfs.datasets imports the pool, creates the dataset when missing, mounts it, and applies its quota. The container activation then downloads the base image to a temporary file, verifies its SHA-256 digest, extracts it into the dataset, copies DNS configuration, and installs the requested packages with apk. The temporary file is removed afterwards, also when the setup fails.
At service start, OpenRC creates a dedicated network namespace, configures loopback and optionally a veth pair, then starts the workload under separate PID, mount, UTS, IPC, and network namespaces. /proc, /run, /tmp, and /dev are mounted inside the container namespace. At service stop, the network namespace and veth interface are removed.
Internet-enabled containers require the host to explicitly set services.nftables.enable = true. They enable IPv4 forwarding and extend the generated nftables configuration with forwarding and masquerade rules. Evaluation rejects internet-enabled containers when nftables is disabled; isolated containers do not require it.
Operations
Use the generated OpenRC service name for normal lifecycle operations.
rc-service container-example status
rc-service container-example start
rc-service container-example restart
rc-service container-example stop
List the enabled containers:
container list
Enter a running container with an interactive shell:
doas container enter example
Run a specific command in the running container:
doas container enter example -- /bin/sh -c 'cat /etc/alpine-release'
container enter joins the running container's PID, mount, UTS, IPC, and network namespaces before changing into its root filesystem. Root permission is required because namespace entry and chroot are privileged operations.
Changes made from the shell are persistent because the writable root filesystem lives in the container's ZFS dataset. Manual package or configuration changes are intentionally possible, but they are not represented by the declarative packages and module settings.
Service logs are written to /var/log/container-<name>/container-<name>.log.
A normal deployment applies storage and root filesystem changes before OpenRC reconciles the generated services.
just switch
Option reference
| Option | Default | Meaning |
|---|---|---|
enable | false | Explicitly creates and runs the container. |
os | "alpine" | Base operating system, "alpine" or "chimera". |
profile | "default" | Common cgroup and networking preset. |
network | null | Optional "internet" or "isolated" override. |
networkId | derived from the container name | Third octet of the internet subnet, 1 to 254. |
dataset | inferred from the root ZFS pool | Explicit ZFS dataset name. |
mountpoint | /var/lib/copland-containers/<name> | Dataset mountpoint. |
quota | "2G" | Dataset quota or null. |
hostname | container attribute name | Hostname inside the UTS namespace. |
command | idle shell loop | Command and arguments executed inside the root filesystem. |
packages | [] | Guest packages installed during deployment. |
environment | {} | Environment passed to the container command. |
cgroup.settings | selected profile | Per-key cgroup v2 overrides. |
alpine.version | "3.24.1" | Alpine minirootfs version. |
alpine.arch | "x86_64" | Alpine architecture. |
alpine.mirror | official Alpine mirror | Base download URL. |
alpine.sha256 | pinned digest | Required minirootfs integrity digest. |
chimera.version | "20251220" | Chimera ROOTFS build date. |
chimera.arch | "x86_64" | Chimera architecture, "x86_64" or "aarch64". |
chimera.mirror | official Chimera mirror | Base download URL. |
chimera.sha256 | pinned digest | Required ROOTFS integrity digest. |