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 on x86_64.
The reusable implementation lives entirely in modules/services/container/. Production host entry points import that provider explicitly; a concrete workload declaration belongs below the selecting host. No container instance is enabled merely by importing the provider.
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 Alpine package names installed with 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 a private 10.240.* subnet, 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.
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 Alpine 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 Alpine release, mirror, architecture, and SHA-256 digest are pinned in the module. Override all relevant values together when intentionally selecting another Alpine minirootfs.
container.custom = {
enable = true;
os = "alpine";
alpine = {
version = "3.24.1";
arch = "x86_64";
mirror = "https://dl-cdn.alpinelinux.org/alpine";
sha256 = "41f73e3cf5fa919b8aa5ca6b30dc48f0da2720776d7423e2a7748211456fe081";
};
};
Deployment refuses to replace an existing dataset with a different base image marker. 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 Alpine minirootfs directly into the dataset, verifies its SHA-256 digest, extracts it, copies DNS configuration, and installs requested Alpine packages with apk.
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 Alpine 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 build
just deploy
Option reference
| Option | Default | Meaning |
|---|---|---|
enable | false | Explicitly creates and runs the container. |
os | "alpine" | Base operating system; currently only Alpine is supported. |
profile | "default" | Common cgroup and networking preset. |
network | null | Optional "internet" or "isolated" override. |
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 BusyBox shell loop | Command and arguments executed inside the root filesystem. |
packages | [] | Alpine 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. |