runc

repository·main·Indexed 11 days ago

https://github.com/opencontainers/runc

A CLI tool for spawning and running containers on Linux according to the OCI (Open Container Initiative) specification. It serves as a low-level container runtime and includes libcontainer, a native Go implementation for managing container lifecycles, namespaces, cgroups, and capabilities. It supports rootless containers and integrates with CRIU (version 1.5.2 or higher) for checkpointing and restoring container states.

Tokens
25.4K
Snippets
94
Records
137
Agent score
94%

What's inside runc

  1. What is libcontainer?

    main

    libcontainer is a native Go implementation for creating containers. It provides the primitives necessary to manage container lifecycles, including the configuration of:

    • Namespaces
    • Cgroups
    • Capabilities
    • Filesystem access controls

    It is designed to allow developers to manage the lifecycle of a container while performing additional operations after the container has been created.

  2. Understand Terminal Modes: New Terminal vs Pass-Through

    main

    runc provides two ways to handle standard IO (stdio):

    1. New Terminal (terminal: true): runc creates a new pseudo-terminal (using the container's namespaced /dev/pts/ptmx). This is the recommended default because it allows tools like sudo to function correctly inside the container. In this mode, stdin, stdout, and stderr all point to the same underlying file to mimic a shell environment.
    2. Pass-Through (terminal: false): runc uses the file handles already provided by the host environment. This is useful for non-interactive pipelines where you want to redirect stdout or stderr to specific files or pipes on the host.

    If the terminal key is missing from config.json, runc defaults to pass-through mode. If runc spec was used to generate the config, it defaults to new terminal mode.

  3. Runtime and Init Process lifecycle

    main

    The container's lifecycle involves a synchronization step between the parent process and the container's init process:

    1. Synchronization: The parent creates a pipe and passes it to the container's init via FD 3. The init process blocks on this pipe until the parent closes its side. This allows the parent to finish setting up cgroups and user namespace mappings (UID/GID) before the application runs.
    2. Execution: The application consuming libcontainer should be compiled statically. The runtime uses exec to start the process inside the container. There should be no long-running init process within the container spec.
    3. I/O Setup: If a pseudo TTY is provided, the runtime opens and dup2s the console as the container's STDIN, STDOUT, and STDERR, and mounts the console at /dev/console.
    4. Runtime Files: The runtime populates extra files such as /etc/hosts, /etc/resolv.conf, /etc/hostname, and /etc/localtime.
  4. Checkpoint and Restore with CRIU

    main

    libcontainer integrates with CRIU to support checkpointing and restoring containers. This allows you to save the state of a running process inside a container to disk and restore that state into a new process, either on the same machine or a different one.

    Requirements:

    • criu version 1.5.2 or higher is required.
    • If criu is not installed, you must install it manually (e.g., building from source) or use a Docker image where criu is pre-installed.
  5. Configure systemd unit name and slice placement

    main

    The name of the systemd unit and its containing slice are derived from the Linux.CgroupsPath field in the container runtime spec (config.json).

    Using Linux.CgroupsPath

    If Linux.CgroupsPath is set, it must follow the format [slice]:[prefix]:[name]:

    • slice: The systemd slice under which the container is placed.
      • Defaults to system.slice.
      • If using cgroup v2 with a rootless container, it defaults to user.slice.
      • Use - to represent a root slice.
      • Sub-slices are denoted with dashes (e.g., user-1000.slice).
      • Note: Slashes (e.g., user.slice/user-1000.slice) are invalid.
    • prefix and name: Used to compose the unit name as <prefix>-<name>.scope.
      • If name has a .slice suffix, the prefix is ignored and the name is used as is.
      • Default values for both are empty strings.

    Default Behavior

    If Linux.CgroupsPath is not set or is empty, runc defaults to the equivalent of :runc:<container-id>.

  6. How to initialize a container with libcontainer

    main

    Container creation in libcontainer follows a two-step process. Because of this, you must provide a binary that will serve as the container's init process.

    In libcontainer, the current binary (/proc/self/exe) is used as the init process by passing the argument init. This initial phase is referred to as the "bootstrap" process. To implement this correctly, your application must include an init function that serves as the entry point for the bootstrap phase.

    Early-stage bootstrapping is handled by importing the nsenter package.

  7. How to use the nsenter package to join namespaces

    main

    The nsenter package provides a special init constructor via Cgo that executes nsexec() before the Go runtime boots. This is necessary to call setns(2) and clone(2) to join Linux namespaces without encountering issues caused by the Go runtime's multi-threaded nature.

    To ensure the nsexec() constructor is executed when your application starts, you must perform a blank import of the package. If you are using libcontainer directly and do not import nsenter, the re-exec process will fail to join the required namespaces.

    Lifecycle of nsexec():

    1. It retrieves the init pipe file descriptor from the _LIBCONTAINER_INITPIPE environment variable.
    2. It reads bootstrap data (namespace paths, clone flags, UID/GID mappings, and console path) from the pipe.
    3. It calls setns(2) to join existing namespaces.
    4. It calls clone(2) with the provided flags (this is required even without CLONE_NEW* flags to allow entering the PID namespace).
    5. It updates UID/GID mappings and performs miscellaneous setup.
    6. It sends the child PID back to the parent and exits, allowing the Go runtime to take over in the newly joined namespaces.
    import _ "github.com/opencontainers/runc/libcontainer/nsenter"
  8. Configure resource allocation with Cgroups

    main

    Cgroups are used to manage system resources like CPU, memory, and device access. The following subsystems are enabled by default in v1 containers:

    SubsystemEnabled
    devices1
    memory1
    cpu1
    cpuacct1
    cpuset1
    blkio1
    perf_event1
    freezer1
    hugetlb1
    pids1

    Synchronization Note: The parent process of the container's init must place the init PID into the correct cgroups before initialization begins. This is synchronized via a pipe (passed to the init process via FD 3) to ensure no processes escape the cgroups during setup.

    | Subsystem  | Enabled |
    | ---------- | ------- |
    | devices    | 1       |
    | memory     | 1       |
    | cpu        | 1       |
    | cpuacct    | 1       |
    | cpuset     | 1       |
    | blkio      | 1       |
    | perf_event | 1       |
    | freezer    | 1       |
    | hugetlb    | 1       |
    | pids       | 1       |
  9. Filesystem requirements and setup for containers

    main

    A container requires a root filesystem (rootfs) to jail and spawn processes. The runtime automatically sets up several essential filesystems within the container's mount namespace.

    Key Filesystem Mounts:

    PathTypeFlagsData
    /procprocMS_NOEXEC,MS_NOSUID,MS_NODEV
    /devtmpfsMS_NOEXEC,MS_STRICTATIMEmode=755
    /dev/shmtmpfsMS_NOEXEC,MS_NOSUID,MS_NODEVmode=1777,size=65536k
    /dev/mqueuemqueueMS_NOEXEC,MS_NOSUID,MS_NODEV
    /dev/ptsdevptsMS_NOEXEC,MS_NOSUIDnewinstance,ptmxmode=0666,mode=620,gid=5
    /syssysfsMS_NOEXEC,MS_NOSUID,MS_NODEV,MS_RDONLY

    Device Nodes: After mounting filesystems, the runtime populates /dev with specific device nodes. A standard rootfs does not need to include these, as the runtime provides them:

    PathModeAccess
    /dev/null0666rwm
    /dev/zero0666rwm
    /dev/full0666rwm
    /dev/tty0666rwm
    /dev/random0666rwm
    /dev/urandom0666rwm

    Standard Symlinks: Once /proc is mounted, the following symlinks are established for I/O:

    • /dev/fd $\rightarrow$ /proc/self/fd
    • /dev/stdin $\rightarrow$ /proc/self/fd/0
    • /dev/stdout $\rightarrow$ /proc/self/fd/1
    • /dev/stderr $\rightarrow$ /proc/self/fd/2

    Root Jailing: The runtime uses pivot_root to jail the process inside the rootfs. If the rootfs is inside a ramfs, MS_MOVE combined with chroot is used instead, as pivot_root is not supported in ramfs.

  10. Understand runc execution modes: Foreground vs Detached

    main

    runc operates in two primary execution modes. Choosing the right one depends on whether you are using the CLI interactively or building a high-level container manager.

    Foreground Mode (Default)

    In this mode, the runc process remains running as a parent to the container process.

    • Behavior: All stdio is buffered through the foreground runc process.
    • Pros: Most straightforward for interactive shell use; handles stdio management for you.
    • Cons: Requires a long-running runc process. If the runc process is killed (e.g., via OOM-kill or manual signal), the container will likely die due to SIGPIPE. It also prevents passing file descriptors directly to the container (unlike --preserve-fds).

    Detached Mode

    In this mode, runc exits once the container has started, leaving no long-running runc process.

    • Behavior: The caller is responsible for managing the container's stdio and acting as a subreaper.
    • Pros: Ideal for high-level tools (like containerd or cri-o) that want full control over the container lifecycle without runc interference.
    • Cons: More complex to implement. The parent process must call prctl(PR_SET_CHILD_SUBREAPER, 1, ...) to prevent zombie processes from accumulating on the host.

    How to run in Detached Mode

    You can trigger detached mode using either of these methods:

    1. Use the -d flag with the run command: runc run -d ...
    2. Follow the OCI lifecycle: runc create (to set up the container) followed by runc start (to begin execution).