SDME(1)

NAME
sdme - lightweight systemd-nspawn containers with overlayfs

SYNOPSIS
sdme [-v] [-c path] command [args...]

DESCRIPTION
sdme manages lightweight containers using systemd-nspawn(1) with overlayfs copy-on-write storage. Each container uses an imported root filesystem as its read-only lower layer, keeping the base rootfs untouched while all modifications are stored in a per-container upper layer.

Containers are managed as systemd template units (sdme@.service) with Type=notify and controlled via D-Bus. The unit is considered active only after systemd-nspawn(1) sends its readiness notification, and a 2-minute startup timeout catches hung boots. Interactive access uses machinectl(1).

sdme requires root privileges. When invoked via sudo(8), the invoking user's configuration file is preferred.

OPTIONS

-v, --verbose
    Enable verbose output. Implies non-interactive mode at runtime.

-c, --config path
    Path to config file. Default: ~/.config/sdme/sdmerc.

COMMANDS

  sdme new [name] [-r fs] [-t seconds] [-u] [limits] [network] [security] [-o dir]... [--pod pod] [--oci-pod pod] [-b bind]... [-e env]... [--oci-env KEY=VALUE]... [--enable] [--no-oci-ports] [--no-oci-volumes] [--] [command...]
Create, start, and enter a new container. If name is omitted, a name is generated automatically. If -r is omitted, the host filesystem is used. Use -t to override the boot timeout (default: 60 seconds or the boot_timeout config value). An optional command overrides the default login shell.

Resource limits can be set with --memory, --cpus, and --cpu-weight; see RESOURCE LIMITS below.

Network options can be set with --private-network, --network-veth, --network-bridge, --network-zone, and --port; see NETWORKING below.

Use -o to make directories opaque in the overlayfs upper layer (hides lower layer contents); see OVERLAYFS OPAQUE DIRS below.

Use -b to add bind mounts and -e to set environment variables; see BIND MOUNTS AND ENVIRONMENT below.

Use --oci-env to set environment variables for the OCI app service (written to /oci/apps/{name}/env in the overlayfs upper layer); see OCI ENVIRONMENT below.

Use --pod to join a pod network namespace (entire container); see PODS below. Use --oci-pod to join a pod network namespace for the OCI app process only; see PODS below.

Use -u / --userns to enable user namespace isolation; see USER NAMESPACES below.

Use --enable to enable auto-start on boot via systemd.

Security hardening options can be set with --strict, --hardened, --drop-capability, --capability, --no-new-privileges, --read-only, --system-call-filter, and --apparmor-profile; see SECURITY HARDENING below.

If boot fails or is interrupted (Ctrl+C), the just-created container is automatically removed.

  sdme create [name] [-r fs] [-u] [limits] [network] [security] [-o dir]... [--pod pod] [--oci-pod pod] [-b bind]... [-e env]... [--oci-env KEY=VALUE]... [--enable] [--no-oci-ports] [--no-oci-volumes]
Create a new container without starting it. Sets up the overlayfs directories and state file. Resource limits can be set with --memory, --cpus, and --cpu-weight; see RESOURCE LIMITS below. Network options can be set with --private-network, --network-veth, --network-bridge, --network-zone, and --port; see NETWORKING below. Use -o to make directories opaque in the overlayfs upper layer; see OVERLAYFS OPAQUE DIRS below. Use -b to add bind mounts and -e to set environment variables; see BIND MOUNTS AND ENVIRONMENT below. Use --oci-env to set environment variables for the OCI app service; see OCI ENVIRONMENT below. Use --pod to join a pod network namespace (entire container); see PODS below. Use --oci-pod to join a pod network namespace for the OCI app process only; see PODS below. Use -u / --userns to enable user namespace isolation; see USER NAMESPACES below.

Use --enable to enable auto-start on boot via systemd.

Security hardening options can be set with --strict, --hardened, --drop-capability, --capability, --no-new-privileges, --read-only, --system-call-filter, and --apparmor-profile; see SECURITY HARDENING below.

Requires a permissive umask (see NOTES below).

  sdme start name... [-t seconds] [--all]
Start one or more stopped containers. Installs or updates the sdme@.service template unit if needed, then starts each unit via D-Bus. Containers are started sequentially; if one fails to boot, it is stopped and the remaining containers are still attempted.

Use --all to start all stopped containers. Use -t to override the boot timeout (default: 60 seconds or the boot_timeout config value).

If boot fails or is interrupted (Ctrl+C), the container is stopped but preserved on disk for debugging.

  sdme set name [--memory size] [--cpus n] [--cpu-weight w]
Set resource limits on an existing container, replacing all current limits. Flags that are not provided are cleared. See RESOURCE LIMITS below for details on each flag.

If the container is running, a note is printed advising a restart for the changes to take effect.

  sdme join [--oci [app]] [--start] [-t seconds] name [--] [command...]
Enter a running container using machinectl(1) shell. An optional command overrides the default login shell.

Use --start to start the container first if it is stopped. Use -t to override the boot timeout when combined with --start.

Use --oci to enter the OCI app's PID, IPC, and mount namespaces via nsenter(1). The default shell is /bin/sh. Inside the app namespaces, commands like ps(1) show only the app's processes, not the container's init tree. For multi-container kube pods, pass the app name: --oci redis. Single-container pods auto-select the only app.

  sdme exec [--oci [app]] name command [args...]
Run a one-off command in a running container using machinectl(1) shell.

Use --oci to enter the OCI app's PID, IPC, and mount namespaces via nsenter(1). The app's host PID is discovered from its cgroup, and nsenter -t pid --pid --ipc --mount --fork is used to enter them. Commands like ps(1) show only the app's processes, not the container's init tree. This is useful for running app-specific tools (e.g. redis-cli, pg_isready). For multi-container kube pods, pass the app name: --oci redis. Single-container pods auto-select the only app.

  sdme stop [-a] [--term] [--kill] name...
Stop one or more running containers. The default mode sends SIGRTMIN+4 to the container leader for a clean shutdown (90 second timeout). Use -a to stop all running containers (cannot be combined with names).

--term
    Terminate mode: sends SIGTERM to the nspawn leader process (30 second timeout).

--kill
    Force-kill mode: sends SIGKILL to all container processes (15 second timeout).

--term and --kill are mutually exclusive.

  sdme rm [-a] [-f] name...
Remove one or more containers. Running containers are stopped first. Deletes the state file and all overlayfs directories. Use -a to remove all containers (cannot be combined with names). Use -f to skip confirmation prompts (useful for scripting).

  sdme ps
List all containers with their status, health, and detected OS. A POD column is shown when any container uses --pod. An OCI-POD column is shown when any container uses --oci-pod. A KUBE column is shown when any container is a kube pod (listing container names). A USERNS column is shown when any container uses --userns. A BINDS column is shown when any container has bind mounts configured. An ENABLED column is shown when any container has auto-start enabled. Health checks detect broken containers (missing directories, missing rootfs). The OS column shows the distro name from the rootfs os-release file.

  sdme enable name...
Enable one or more containers to start automatically on boot. Creates a systemd wants symlink so the container's unit starts at boot time.

  sdme disable name...
Disable auto-start for one or more containers. Removes the systemd wants symlink.

  sdme logs [--oci [app]] name [args...]
View container logs by executing journalctl(1) for the container's service unit. Extra arguments are passed through to journalctl (e.g. -f, -n 100).

Use --oci to show logs for the OCI app service (sdme-oci-{name}.service) instead of the container unit. This runs journalctl inside the container via machinectl(1). For multi-container kube pods, pass the app name: --oci redis. Single-container pods auto-select the only app.

  sdme fs import name source [-f] [--install-packages mode] [--oci-mode mode] [--base-fs rootfs]
Import a root filesystem. After import, systemd is detected in the rootfs; if missing, distro-specific packages can be installed via chroot. The --install-packages flag controls this: auto (default) prompts interactively; in non-interactive mode (e.g. with -v), refuses and suggests re-running with --install-packages=yes or -f. yes always installs. no refuses if systemd is absent.

For OCI registry images, the --oci-mode flag controls how the image is classified:

    auto
        (default) Auto-detect from image config. Base OS images have no entrypoint, a shell as default command, and no exposed ports. Everything else is treated as an application image.

    base
        Force base OS mode. The rootfs goes through systemd detection and package installation. Use this for OS images that the heuristic misclassifies.

    app Force application mode. Requires --base-fs to specify a systemd-capable rootfs as the base layer.

The --base-fs flag specifies a previously imported rootfs (with systemd) to use as the base layer for OCI application images. The application rootfs is placed under /oci/apps/{name}/root inside a copy of the base, and a systemd service unit (sdme-oci-{name}.service) is generated to run the application's entrypoint via RootDirectory=/oci/apps/{name}/root. OCI metadata (environment, ports, volumes) is saved under /oci/. See Section 6 (OCI Integration) in docs/architecture.md for details.

The source type is auto-detected:

    •   URL (http:// or https://): download and extract tarball.

    •   Registry image (registry/repo:tag): pull from an OCI-compatible registry using the OCI Distribution Spec (e.g. quay.io/fedora/fedora:latest). Supports anonymous bearer token authentication and manifest list platform selection.

    •   Directory: copy the directory tree.

    •   QCOW2 image (detected by magic bytes): mount via qemu-nbd(8) and copy the filesystem tree.

    •   Tarball (.tar, .tar.gz, .tar.bz2, .tar.xz, .tar.zst): extract using native Rust crates with magic-byte compression detection. OCI container images are auto-detected after extraction and their filesystem layers are applied in order with whiteout handling.

Use -f to remove a leftover staging directory from a previous failed import.

  sdme fs ls
List imported root filesystems with distro detection and paths.

  sdme fs rm [-a] [-f] name...
Remove one or more imported root filesystems. Use -a to remove all imported root filesystems (cannot be combined with names). Use -f to skip confirmation prompts.

  sdme fs build name build.conf [-t seconds] [-f]
Build a root filesystem from a build config file. Creates a temporary staging container from the base rootfs specified by FROM, executes RUN and COPY directives sequentially, then captures the resulting merged filesystem as a new rootfs named name.

Use -t to override the boot timeout (default: 60 seconds or the boot_timeout config value). Use -f to replace an existing rootfs with the same name.

The staging container is automatically removed after the build completes or on failure/Ctrl+C.

The build config is a line-oriented text file with three directives:

    FROM rootfs
        Base rootfs to build on (must be first directive, required, only once). Must refer to a rootfs previously imported with sdme fs import.

    RUN command
        Run a shell command inside the container via /bin/sh -c. Supports pipes, redirects, and compound commands. The container is started automatically if not already running.

    COPY src dst
        Copy a host file or directory into the rootfs. The container is stopped before copying (overlayfs constraint). If dst is an existing directory, the source is placed inside it. Destination paths with .. components are rejected.

    Because COPY writes directly to the overlayfs upper layer (while the container is stopped), destinations under directories that systemd mounts tmpfs over at boot are rejected: /tmp, /run, /dev/shm. Files placed there would be hidden by the tmpfs and invisible inside the running container. Destinations under overlayfs opaque directories are also rejected. Use a different path (e.g. /root, /opt, /srv).

Lines starting with # and blank lines are ignored.

  sdme fs cache info
Show the OCI blob cache location, total size, and blob count.

  sdme fs cache ls
List all cached OCI blobs with their digest and size.

  sdme fs cache clean [--all]
Clean up the OCI blob cache. By default, removes only expired blobs (based on LRU eviction policy). Use --all to remove all cached blobs.

  sdme pod new name
Create a new pod network namespace. Allocates a network namespace with loopback up, bind-mounts it to /run/sdme/pods/name/netns, and writes persistent state to /var/lib/sdme/pods/name/state.

  sdme pod ls
List all pods with their active/inactive status and creation timestamp. A pod is active when its runtime bind-mount exists under /run/sdme/pods/.

  sdme pod rm [-f] name...
Remove one or more pods. Unmounts the runtime netns, removes the runtime directory, and deletes the persistent state directory. Refuses if any container references the pod (via POD or OCI_POD keys) unless -f is given.

  sdme kube apply -f file [--base-fs name] [--pod pod] [--oci-pod pod] [-t seconds] [security]
Create, start, and enter a container from a Kubernetes Pod YAML file. Equivalent to kube create followed by start and join.

The YAML file must be a v1 Pod or apps/v1 Deployment manifest. The --base-fs flag specifies a previously imported rootfs (with systemd) to use as the base layer. Defaults to the default_base_fs config value. Use -t to override the boot timeout.

Security hardening options can be set with --strict, --hardened, --drop-capability, --capability, --no-new-privileges, --read-only, --system-call-filter, and --apparmor-profile; see SECURITY HARDENING below.

If boot fails or is interrupted (Ctrl+C), the just-created container is automatically removed.

  sdme kube create -f file [--base-fs name] [--pod pod] [--oci-pod pod] [security]
Create a container from a Kubernetes Pod YAML file without starting it.

Parses the YAML, pulls each container's OCI image from the registry, builds a combined rootfs under kube-{podname}, and creates an sdme container. Each pod container becomes a separate systemd service (sdme-oci-{name}.service) chrooted into its own rootfs under /oci/apps/{name}/root.

Security hardening options can be set with --strict, --hardened, --drop-capability, --capability, --no-new-privileges, --read-only, --system-call-filter, and --apparmor-profile; see SECURITY HARDENING below.

Supported volume types:

    emptyDir
        Creates a shared directory at /oci/volumes/{name} inside the rootfs. A generated oneshot service (sdme-kube-volumes.service) bind-mounts each volume into the appropriate app root directories before app services start. Read-only volume mounts are remounted read-only after the initial bind.

    hostPath
        Bind-mounted into the container at the nspawn level via --bind=.

    secret
        Populated from an sdme kube secret (see sdme kube secret create). Supports items for projected key paths and defaultMode for file permissions.

    configMap
        Populated from an sdme kube configmap (see sdme kube configmap create). Supports items and defaultMode.

    persistentVolumeClaim
        Host directory at {datadir}/volumes/{claimName} bind-mounted into the container via --bind=.

Init containers (initContainers) run to completion (as oneshot services) before app containers start.

Per-container resource limits (resources) map to systemd properties: MemoryMax, MemoryLow, CPUQuota, and CPUWeight.

Readiness probes (exec only) are translated to ExecStartPre commands in the generated service unit. Liveness probes are parsed but not yet enforced at runtime.

Environment variables support valueFrom with secretKeyRef and configMapKeyRef for deferred resolution from secrets and configmaps at create time. envFrom bulk-imports all keys from a configMap or secret as environment variables, with an optional prefix prepended to each key. Explicit env[] entries take priority over envFrom for the same key.

Pod-level securityContext supports runAsUser, runAsGroup, and runAsNonRoot.

Ports declared across all containers are aggregated; if any ports are declared, --private-network is enabled automatically.

The restart policy maps to systemd's Restart= directive: Always=always, OnFailure=on-failure, Never=no.

  sdme kube delete [--force] name
Stop and remove a kube pod container, then remove its rootfs. Refuses to delete non-kube containers unless --force is given.

  sdme kube secret create name --from-literal KEY=VALUE [--from-file KEY=PATH]
Create a secret from literal key=value pairs or file contents. At least one --from-literal or --from-file is required. Both flags are repeatable. Secrets are stored at {datadir}/secrets/{name}/data/{key} with restricted permissions (0700 directories, 0600 files).

  sdme kube secret ls
List all secrets with name, key count, and creation time.

  sdme kube secret rm name...
Remove one or more secrets.

  sdme kube configmap create name --from-literal KEY=VALUE [--from-file KEY=PATH]
Create a configmap from literal key=value pairs or file contents. At least one --from-literal or --from-file is required. Both flags are repeatable. Configmaps are stored at {datadir}/configmaps/{name}/data/{key} with standard permissions (0755 directories, 0644 files).

  sdme kube configmap ls
List all configmaps with name, key count, and creation time.

  sdme kube configmap rm name...
Remove one or more configmaps.

  sdme config get
Show current configuration values.

  sdme config apparmor-profile
Print the default AppArmor profile for sdme containers to stdout. The profile is named sdme-default and is designed for systemd-nspawn system containers. To install:

    sdme config apparmor-profile > /etc/apparmor.d/sdme-default
    apparmor_parser -r /etc/apparmor.d/sdme-default

The profile is automatically applied when using --strict. It can also be applied manually with --apparmor-profile sdme-default. The deb and rpm packages install and load the profile automatically.

  sdme config completions shell
Generate shell completions for the specified shell. Supported shells: bash, fish, zsh. Example:

    sdme config completions bash > /etc/bash_completion.d/sdme
    sdme config completions zsh > /usr/local/share/zsh/site-functions/_sdme
    sdme config completions fish > ~/.config/fish/completions/sdme.fish

  sdme config set key value
Set a configuration value. Valid keys:

    interactive
        Enable or disable interactive mode. Values: yes or no. Default: yes.

    datadir
        Absolute path to the data directory. Default: /var/lib/sdme.

    boot_timeout
        Boot timeout in seconds. Values: positive integer. Default: 60.

    join_as_sudo_user
        Automatically join host-rootfs containers as the SUDO_USER instead of root. Values: yes or no. Default: yes.

    host_rootfs_opaque_dirs
        Comma-separated list of directories to make opaque by default when creating host-rootfs containers (no -r). Explicit -o flags override this setting. Set to an empty string to disable. Default: /etc/systemd/system,/var/log.

    hardened_drop_caps
        Comma-separated list of capabilities to drop when --hardened is used. Set to an empty string to disable all hardened capability drops. Default: CAP_SYS_PTRACE,CAP_NET_RAW,CAP_SYS_RAWIO,CAP_SYS_BOOT.

    default_base_fs
        Name of the default base rootfs for OCI app imports and kube pods. When set, the --base-fs flag can be omitted from sdme fs import, sdme kube apply, and sdme kube create. Default: empty (not set).

CONFIGURATION
The configuration file is in TOML format, located at ~/.config/sdme/sdmerc. When running under sudo(8), the invoking user's config is preferred if it exists. The path can also be set via the -c option or the XDG_CONFIG_HOME environment variable.

Available keys:

interactive
    Boolean. Whether to run in interactive mode. Default: true.

datadir
    String. Absolute path to the data directory. Default: /var/lib/sdme.

boot_timeout
    Integer. Number of seconds to wait for a container to finish booting before giving up. Can be overridden per-command with -t/--timeout. Default: 60.

join_as_sudo_user
    Boolean. When true and the container uses the host root filesystem, sdme join and sdme exec pass --uid SUDO_USER to machinectl(1) so the session runs as the invoking user rather than root. Containers created with an imported rootfs are unaffected. Default: true.

host_rootfs_opaque_dirs
    String. Comma-separated list of absolute directory paths to make opaque in the overlayfs upper layer when creating host-rootfs containers (i.e., when -r is not specified). Opaque directories hide all lower-layer contents, so they start empty in the container. If the user passes one or more -o flags at creation time, this config value is ignored. Containers created with an imported rootfs are unaffected. Set to an empty string to disable default opaque dirs. Default: /etc/systemd/system,/var/log.

hardened_drop_caps
    String. Comma-separated list of Linux capabilities to drop when --hardened is used. Capabilities are validated against the known set. Set to an empty string to disable all hardened capability drops. Default: CAP_SYS_PTRACE,CAP_NET_RAW,CAP_SYS_RAWIO,CAP_SYS_BOOT.

default_base_fs
    String. Name of a previously imported rootfs (with systemd) to use as the default base layer for OCI app imports and kube pods. When set, the --base-fs flag can be omitted from sdme fs import, sdme kube apply, and sdme kube create. Default: empty (not set).

FILES

/var/lib/sdme/
    Default data directory (configurable via datadir).

/var/lib/sdme/fs/name/
    Imported root filesystems (lower layers).

/var/lib/sdme/state/name
    Container state files (KEY=VALUE format). Contains container metadata including creation timestamp, rootfs name, resource limits (MEMORY, CPUS, CPU_WEIGHT), network configuration (PRIVATE_NETWORK, NETWORK_VETH, NETWORK_BRIDGE, NETWORK_ZONE, PORTS), opaque directories (OPAQUE_DIRS), bind mounts (BINDS), environment variables (ENVS), pod membership (POD, OCI_POD), user namespace isolation (USERNS), OCI volumes (OCI_VOLUMES), kube pod metadata (KUBE, KUBE_CONTAINERS, KUBE_YAML_HASH), and security hardening (DROP_CAPS, ADD_CAPS, NO_NEW_PRIVS, READ_ONLY, SYSCALL_FILTER, APPARMOR_PROFILE).

/var/lib/sdme/volumes/name/
    Per-container OCI volume data (created automatically for OCI images that declare volumes). Survives container removal; must be deleted manually to reclaim disk space.

/var/lib/sdme/containers/name/upper/
    Per-container overlayfs upper layer (copy-on-write modifications).

/var/lib/sdme/containers/name/work/
    Per-container overlayfs work directory.

/var/lib/sdme/containers/name/merged/
    Per-container overlayfs merged mount point.

/var/lib/sdme/secrets/name/data/key
    Kube secret data files (permissions 0600).

/var/lib/sdme/configmaps/name/data/key
    Kube configmap data files (permissions 0644).

/var/lib/sdme/pods/name/state
    Pod persistent state file (KEY=VALUE format).

/run/sdme/pods/name/netns
    Pod runtime bind-mount of the network namespace fd (volatile, recreated after reboot when a container referencing the pod is started).

/etc/systemd/system/sdme@.service
    Systemd template unit (auto-installed and auto-updated). Uses Type=notify so the service is considered started only after systemd-nspawn(1) signals readiness. Exit code 133 is treated as a successful clean shutdown.

/etc/systemd/system/sdme@name.service.d/limits.conf
    Per-container resource limits drop-in (created when limits are set, removed when limits are cleared or the container is deleted).

~/.config/sdme/sdmerc
    User configuration file (TOML format).

ENVIRONMENT

SUDO_USER
    When set, sdme reads the invoking user's configuration file and resolves their home directory for config file lookup.

XDG_CONFIG_HOME
    Overrides the base directory for the configuration file. Default: ~/.config.

HOME
    Used to locate the configuration file when XDG_CONFIG_HOME is not set.

https_proxy, HTTPS_PROXY, http_proxy, HTTP_PROXY, all_proxy, ALL_PROXY
    Proxy URI for HTTP/HTTPS downloads during sdme fs import. The first non-empty variable found (in the order listed) is used. Example: http://proxy:3128. Since sdme requires root, pass proxy variables through sudo(8) with -E or by setting them inline.

no_proxy, NO_PROXY
    Comma-separated list of hosts or domains that should bypass the proxy.

EXIT STATUS

0   Success.

non-zero
    An error occurred. Error messages are printed to standard error.

EXAMPLES
Import an Ubuntu rootfs from a URL and create a container:

    sdme fs import ubuntu https://example.com/ubuntu.tar.xz
    sdme new mybox -r ubuntu

Import a rootfs from an OCI registry:

    sdme fs import fedora quay.io/fedora/fedora
    sdme new -r fedora

Import an OCI application image and run it inside a base container:

    sdme fs import ubuntu docker.io/ubuntu:24.04
    sdme fs import nginx docker.io/nginx --base-fs=ubuntu
    sdme new -r nginx

Run a command in a running container:

    sdme exec mybox cat /etc/os-release

Create a container with resource limits:

    sdme new --memory 2G --cpus 2 mybox -r ubuntu

Update limits on an existing container:

    sdme set mybox --memory 4G

Follow logs for a container:

    sdme logs mybox -f

Import a rootfs through an HTTP proxy:

    sudo -E sdme fs import ubuntu https://example.com/ubuntu.tar.xz

Or pass the proxy variable inline:

    sudo https_proxy=http://proxy:3128 sdme fs import ubuntu https://example.com/ubuntu.tar.xz

Create a container with a private network, veth, and port forwarding:

    sdme new --private-network --network-veth --port 8080:80 webserver -r ubuntu

Create containers in a shared network zone:

    sdme new --network-zone myzone db -r ubuntu
    sdme new --network-zone myzone app -r ubuntu
    # app and db can now communicate over the shared zone network

Create a container with bind mounts and environment variables:

    sdme new -b /data:/mnt/data -e LANG=C.UTF-8 mybox -r ubuntu

Build a custom rootfs from an imported base:

    # Import a base rootfs
    debootstrap --include=dbus,systemd noble /tmp/ubuntu
    sdme fs import ubuntu /tmp/ubuntu
    # Create a build config
    cat << EOF > examplefs.conf
    FROM ubuntu
    RUN apt-get update
    RUN apt-get install -y systemd-container
    COPY ./target/release/sdme /usr/local/bin/sdme
    EOF
    # Build the new rootfs and create a container
    sdme fs build examplefs examplefs.conf
    sdme new -r examplefs

Create a hardened container with user namespace isolation and private network:

    sdme new --hardened -r ubuntu

Create a container with maximum security (Docker-equivalent restrictions):

    sdme new --strict -r ubuntu

Create a container with fine-grained security controls:

    sdme new --drop-capability CAP_NET_RAW --no-new-privileges --read-only -r ubuntu

Run two containers in a shared pod network namespace:

    sdme fs import ubuntu docker.io/ubuntu:24.04
    sdme pod new my-pod
    sdme new --pod=my-pod -r ubuntu db
    sdme new --pod=my-pod -r ubuntu app
    # db and app can communicate via localhost

Create and start a Kubernetes pod from a YAML file:

    sdme fs import ubuntu docker.io/ubuntu:24.04
    sdme kube apply -f my-pod.yaml --base-fs ubuntu

Delete a Kubernetes pod (stops container, removes rootfs):

    sdme kube delete my-pod

Run two OCI app containers in a shared pod network namespace:

    sdme fs import nginx docker.io/nginx --base-fs=ubuntu
    sdme pod new oci-pod
    sdme new --oci-pod=oci-pod --hardened -r nginx web
    # nginx binds port 80 on the pod's loopback

RESOURCE LIMITS
Containers can have per-container resource limits applied via systemd cgroup controls. Limits are stored in the container's state file and applied at start time through a systemd drop-in file at /etc/systemd/system/sdme@name.service.d/limits.conf.

If no limits are set, no drop-in is written.

--memory size
    Maximum memory the container may use. Maps to the systemd MemoryMax= directive. Accepts a number with an optional suffix: K, M, G, T. Example: 512M, 2G.

--cpus n
    CPU time limit expressed as a number of CPUs. Converted to the systemd CPUQuota= directive (e.g. 2 becomes 200%). Accepts integers or decimals. Example: 2, 0.5.

--cpu-weight w
    Relative CPU scheduling weight. Maps to the systemd CPUWeight= directive. Range: 1-10000 (default kernel weight is 100). Lower values get proportionally less CPU time under contention.

NETWORKING
By default, containers share the host's network namespace and have full network access. Network options control network isolation and connectivity. These options are set at container creation time and cannot be changed afterward.

--private-network
    Run the container in its own private network namespace, isolated from the host. Without additional options, the container will only have a loopback interface.

--network-veth
    Create a virtual ethernet pair connecting the container to the host. Implies --private-network. The host side of the veth is named ve-<name>. Requires network configuration inside the container (DHCP or static IP) and NAT/routing on the host for external connectivity.

--network-bridge name
    Connect the container's veth interface to the specified host bridge. Implies --private-network. The bridge must already exist on the host. Example: br0.

--network-zone name
    Join a named network zone for inter-container networking. Implies --private-network. Containers in the same zone can communicate via a virtual bridge that systemd-nspawn(1) creates automatically. Example: myzone.

--port, -p [proto:]host[:container]
    Forward a port from the host to the container. Implies --private-network. The protocol prefix is tcp (default) or udp. If the container port is omitted, the host port is used for both. Can be specified multiple times for multiple ports. Port forwarding requires a virtual ethernet pair to route traffic; combine with --network-veth, --network-bridge, or --network-zone. The container must run systemd-networkd(8) to configure the container-side interface via DHCP. Example: 8080:80, tcp:8080:80, udp:53:53.

Network configuration is stored in the container's state file and applied at start time via environment variables in the systemd unit.

OVERLAYFS OPAQUE DIRS
Directories listed with -o (or --overlayfs-opaque-dirs) are created in the overlayfs upper layer with the trusted.overlay.opaque extended attribute set to y. This makes overlayfs hide all lower-layer contents for that directory, so it starts empty inside the container. Can be specified multiple times.

Each path must be absolute, must not contain .. components, and duplicates are rejected. Trailing slashes are normalized.

For host-rootfs containers (no -r), if no -o flags are given, the host_rootfs_opaque_dirs config value is applied automatically (default: /etc/systemd/system,/var/log). This prevents the host's systemd unit overrides and log history from leaking into the container. Set the config value to an empty string to disable this behavior. For imported-rootfs containers, no default opaque dirs are applied.

BIND MOUNTS AND ENVIRONMENT

-b HOST:CONTAINER[:ro]
--bind HOST:CONTAINER[:ro]
    Bind mount a host path into the container. Both paths must be absolute and must not contain .. components. Append :ro for a read-only mount. Can be specified multiple times. Example: /data:/mnt/data, /config:/etc/app:ro.

-e KEY=VALUE
--env KEY=VALUE
    Set an environment variable for the container. Can be specified multiple times. Example: LANG=C.UTF-8.

Bind mounts and environment variables are stored in the container's state file and applied at start time via the systemd nspawn drop-in configuration.

OCI ENVIRONMENT

--oci-env KEY=VALUE
    Set an environment variable for the OCI app service. Can be specified multiple times. The value is written to /oci/apps/{name}/env in the container's overlayfs upper layer. The sdme-oci-{name}.service unit reads this file via EnvironmentFile=.

This is separate from -e/--env, which sets environment variables for the container's systemd init (PID 1) via nspawn --setenv= flags. Use --oci-env for variables the OCI application needs (e.g. POSTGRES_PASSWORD, REDIS_ARGS).

Requires an OCI app rootfs (the rootfs must contain /oci/apps/{name}/env). Example:

    sdme create -r postgresql --oci-env POSTGRES_PASSWORD=secret mydb

OCI PORT FORWARDING
When creating a container from an OCI app rootfs that declares exposed ports (in /oci/ports), sdme automatically adds port forwarding rules if the container has a private network namespace (via --private-network, --hardened, or --strict). Each OCI port is mapped from the same host port to the same container port (e.g. tcp:80:80).

Port forwarding requires a virtual ethernet pair to route traffic between host and container. Combine --private-network with --network-veth, --network-bridge, or --network-zone for ports to be reachable. The container must have systemd-networkd(8) enabled so the container-side interface (host0) gets configured via DHCP.

User-supplied --port flags take priority: if you map a container port manually, the auto-forward for that port is skipped.

When the container uses host networking (no --private-network), ports are not forwarded (services bind directly to the host's interfaces). An informational message listing the exposed ports is printed instead.

--no-oci-ports
    Suppress automatic port forwarding from OCI image metadata. Ports declared in the image's /oci/ports file are ignored.

OCI VOLUME MOUNTS
When creating a container from an OCI app rootfs that declares volumes (in /oci/volumes), sdme automatically creates persistent host-side directories and adds bind mounts for each volume.

For each declared volume path (e.g. /var/lib/mysql), a host directory is created at /var/lib/sdme/volumes/name/vol-name and bind-mounted to /oci/apps/{name}/root/var/lib/mysql inside the container. Volume data persists across container removal; when sdme rm deletes a container with OCI volumes, the volume directory path is printed but the data is not deleted.

User-supplied --bind flags take priority: if you bind-mount to the same container path that an OCI volume targets, the auto-mount for that path is skipped.

--no-oci-volumes
    Suppress automatic volume mounting from OCI image metadata. Volumes declared in the image's /oci/volumes file are ignored.

PODS
Pods provide a shared network namespace for multiple containers. Containers in the same pod can reach each other via localhost.

Create a pod, then assign containers to it:

    sdme pod new my-pod
    sdme create --pod=my-pod -r ubuntu db
    sdme create --pod=my-pod -r ubuntu app
    sdme start db app

There are two mechanisms for joining a pod:

--pod name
    Join the named pod's network namespace (entire container). The whole nspawn container, including init and all services, runs in the pod's netns via --network-namespace-path=. Works with any container type (host-rootfs or imported rootfs). Mutually exclusive with --private-network. Pod membership is stored in the container's state file as POD.

--oci-pod name
    Join the named pod's network namespace for the OCI app process only. The pod's netns is bind-mounted into the container, and an inner systemd drop-in makes only the sdme-oci-{name}.service process enter the pod's netns via NetworkNamespacePath=. The container's init and other services remain in their own network namespace. Requires an OCI app rootfs (the rootfs must contain an sdme-oci-{name}.service unit) and --private-network (or --hardened / --strict, which imply it). Pod membership is stored in the container's state file as OCI_POD.

Both flags are available on sdme create and sdme new. They can be combined on the same container with different pods. The pod's network namespace contains only a loopback interface.

USER NAMESPACES

-u, --userns
    Enable user namespace isolation. When enabled, container root (UID 0) maps to an unprivileged high UID on the host (524288+, deterministically hashed from the container's machine name). This prevents a container escape from gaining host root privileges.

Uses systemd-nspawn(1) flags --private-users=pick and --private-users-ownership=auto. On kernel 6.6+, overlayfs supports idmapped mounts, so files in the upper layer stay UID 0 on disk while the container runs as a high UID on the host (zero overhead). On older kernels, auto falls back to recursive chown.

User namespace isolation is set at creation time and stored in the container's state file as USERNS=yes.

SECURITY HARDENING
Security hardening flags are available on sdme create and sdme new. They are stored in the container's state file and applied at start time.

--strict
    Maximum security. Implies --hardened and adds Docker-equivalent capability drops (retains only ~14 capabilities plus CAP_SYS_ADMIN for systemd init), seccomp filters (~@cpu-emulation, ~@debug, ~@obsolete, ~@raw-io), and the sdme-default AppArmor profile. The AppArmor profile must be loaded before use; see sdme config apparmor-profile.

--hardened
    Enable defense-in-depth defaults: user namespace isolation (--private-users=pick), private network namespace, no-new-privileges, and capability drops (configurable via the hardened_drop_caps config key; default: CAP_SYS_PTRACE,CAP_NET_RAW,CAP_SYS_RAWIO,CAP_SYS_BOOT).

--drop-capability cap
    Drop a capability from the container's bounding set. Accepts names with or without the CAP_ prefix. Can be specified multiple times. Example: CAP_SYS_PTRACE, NET_RAW.

--capability cap
    Add a capability to the container's bounding set (for capabilities not in the default nspawn set). Accepts names with or without the CAP_ prefix. Can be specified multiple times. Specifying the same capability in both --drop-capability and --capability is rejected. Example: CAP_NET_ADMIN.

--no-new-privileges
    Prevent processes inside the container from gaining privileges via setuid binaries or file capabilities. Passes --no-new-privileges=yes to systemd-nspawn(1). Note that this breaks sudo(8) and su(1) inside the container.

--read-only
    Mount the container's overlayfs merged view as read-only. Applications that need writable areas should use bind mounts (-b).

--system-call-filter filter
    Apply an additional seccomp filter on top of nspawn's baseline. Uses systemd's group syntax: @group to allow, ~@group to deny. Can be specified multiple times. Example: ~@raw-io, ~@cpu-emulation. Note that ~@mount will break systemd inside the container.

--apparmor-profile name
    Apply an AppArmor profile to the container via the AppArmorProfile= directive in the systemd service unit drop-in. The profile must be loaded into the kernel before use. Example: sdme-default.

--strict and --hardened set baselines that individual flags can override. For example, --hardened --capability CAP_NET_RAW re-enables a capability that --hardened would otherwise drop.

NOTES

  Umask
sdme create and sdme new require a umask that does not strip read or execute from "other". If the current umask has bits 005 set (e.g. 007, 027, 077), creation is refused with a diagnostic message.

This is because files written to the overlayfs upper layer inherit the process umask, and non-root services inside the container (such as dbus-daemon running as messagebus) need to traverse the filesystem. The default umask (022) is sufficient.

  Ctrl+C during boot
Pressing Ctrl+C while sdme new or sdme start is waiting for the container to boot will cancel the wait. For sdme new, the just-created container is removed automatically. For sdme start, the container is stopped but preserved on disk.

SECURITY
sdme runs as root and processes untrusted input (tarballs, OCI images, disk images, URLs). The following hardening measures are in place:

  Path traversal protection
OCI layer tar paths are sanitized before whiteout marker handling: entries containing .. components are rejected, and leading / is stripped. OCI digest fields in manifests are validated to contain only alphanumeric and hex characters before being used to resolve blob paths. The --rootfs parameter is validated against the same naming rules as container names, preventing directory traversal via crafted values like ../state.

  Download size limit
URL downloads during sdme fs import are limited to 50 GiB to prevent disk exhaustion from malicious or misbehaving servers. Downloads exceeding this limit are aborted with an error.

  File permissions
The configuration file is written with mode 0600 and its parent directories with mode 0700, regardless of the process umask. State files are created atomically with O_CREAT|O_EXCL.

SEE ALSO
systemd-nspawn(1), machinectl(1), nsenter(1), journalctl(1), qemu-nbd(8), overlayfs(5), sudo(8)