---
title: "Build a Tiny Linux Container without Docker in 2026"
description: "A hands-on 2026 walkthrough for building a tiny isolated Linux container with overlayFS, cgroups, namespaces, pivot_root, and kernel primitives instead of Docker."
author: Aleksei Aleinikov
date: 2026-06-18
updated: 2026-06-28
lang: en
tags: [containers, linux, docker, namespaces, cgroups, devops]
canonical: https://www.alekseialeinikov.com/en/blog/topics/devops/build-a-tiny-linux-container-without-docker-2026
source: alekseialeinikov.com
---

# Build a Tiny Linux Container without Docker in 2026

Containers feel mysterious until you build one yourself.

In this walkthrough, we will assemble a tiny isolated Linux environment using primitives that already exist in the kernel: overlayFS, **cgroup**s, namespaces, and `pivot_root`. No **Docker**, no Podman, no **container** runtime.

![When you decide to operate directly on the Linux kernel primitives today.](https://www.alekseialeinikov.com/blog/1_PVv90FbaIT1ESF_fGrDs9g.webp)

Use a disposable virtual machine for this experiment. We will run privileged commands, create device nodes, mount filesystems, and change root directories. It is very easy to break a host system by accident.

The point is not to replace Docker in production. The point is to understand what Docker and similar tools are wrapping for you.

> Once these primitives click, the natural next step is a tool that productizes them without a root daemon — see [Podman in 2026: Rootless, Daemonless Containers](https://www.alekseialeinikov.com/en/blog/topics/devops/podman-2026-rootless-daemonless-containers-without-docker), which turns exactly this `unshare` dance into a friendly, rootless workflow.

## The Kernel Pieces Behind a Container

**A container is not a single magic feature.** It is a combination of Linux isolation and resource-control mechanisms:

- overlayFS gives the container a writable filesystem layer,
- cgroups enforce CPU and memory limits,
- namespaces isolate process IDs, hostnames, mounts, networking, IPC, and cgroup views,
- `pivot_root` switches the process into a new root filesystem.

The term control group, or cgroup, first appeared in the Linux kernel in 2007. It was originally known as process containers, which is a nice reminder that container tooling came after the kernel ideas.

## Preparing a Private Root Filesystem

Create a directory structure for the experiment:

```bash
mkdir -p /tmp/mybox/{lower,upper,work,merged}
```

Download a small root filesystem archive such as Alpine, BusyBox, or a similar minimal rootfs, then unpack it into `lower`:

```bash
tar -xzf mini-rootfs.tar.gz -C /tmp/mybox/lower
```

Now layer it with overlayFS so changes stay in `upper` while the original files remain untouched:

```bash
mount -t overlay overlay \
  -o lowerdir=/tmp/mybox/lower,upperdir=/tmp/mybox/upper,workdir=/tmp/mybox/work \
  /tmp/mybox/merged
```

`/tmp/mybox/merged` now acts as the container's root directory.

Because overlayFS stores only modified files, a container can share a read-only base and keep a small writable layer. This is one reason container images can be efficient even when many containers share the same base.

## Setting Resource Limits with cgroups

**This example assumes cgroup v2.**

Create a new cgroup and allow CPU and memory controls inside it:

```bash
mkdir -p /sys/fs/cgroup/mybox.slice/one
echo "+memory +cpu" > /sys/fs/cgroup/mybox.slice/cgroup.subtree_control
```

Cap the container at 15 percent of one CPU core and 512 MiB of RAM. Disable swap for the experiment:

```bash
echo "15000 100000" > /sys/fs/cgroup/mybox.slice/one/cpu.max
echo "512M"         > /sys/fs/cgroup/mybox.slice/one/memory.max
echo "0"            > /sys/fs/cgroup/mybox.slice/one/memory.swap.max
```

Why `15000 100000`?

The first number is runtime in microseconds. The second number is the total period. The process may run for 15,000 microseconds out of every 100,000 microseconds, which is exactly 15 percent.

## Entering New Namespaces

Switch to root, join the cgroup, then spawn a shell inside fresh namespaces:

```bash
sudo -i
echo $$ > /sys/fs/cgroup/mybox.slice/one/cgroup.procs

unshare \
  --uts --pid --mount --mount-proc \
  --net --ipc --cgroup \
  --fork /bin/bash
```

Now you have several isolated views:

- UTS **namespace**: choose a hostname that is visible only inside the container.
- PID namespace: the first process inside gets PID 1.
- Mount namespace: mount points are private to this environment.
- Network namespace: networking can be separated from the host.
- IPC namespace: inter-process communication is isolated.

Try changing the hostname:

```bash
hostname mycontainer2026
```

That hostname is visible only inside this namespace.

## Switching Roots with pivot_root

Move into the merged directory, make the mount tree private, pivot into the new root, and detach the old one:

```bash
cd /tmp/mybox/merged
mount --make-rprivate /
mkdir old_root
pivot_root . old_root
umount -l /old_root
rmdir old_root
```

`pivot_root` is stronger than a casual `chroot` demo because the old root can be detached. That prevents several classic escape patterns where the former root remains reachable.

## Mounting Essential Virtual Filesystems

Without `/proc`, `/sys`, and `/dev`, many tools fail.

Create a minimal set:

```bash
mknod -m 666 dev/null c 1 3
mknod -m 666 dev/zero c 1 5
mknod -m 666 dev/tty  c 5 0

mkdir -p dev/{pts,shm}
mount -t devpts devpts dev/pts
mount -t tmpfs  tmpfs  dev/shm
mount -t sysfs  sysfs  sys
mount -t tmpfs  tmpfs  run
mount -t proc   proc   proc
```

This is still not a production-grade container environment. It is a learning environment. Real runtimes add careful mount policies, **seccomp**, capabilities, AppArmor or SELinux, user namespaces, image management, networking, and cleanup logic.

## Replacing the Shell with an Application

For the demo, start a minimal shell from the root filesystem:

```bash
exec /bin/busybox sh
```

At this point, you are inside a self-made container-like environment.

The process has an isolated PID view, a private hostname, its own mount namespace, a separate root filesystem, and cgroup limits.

## Verifying the Limits

### CPU Test

Inside the container, run a tight loop:

```bash
while :; do :; done
```

On the host, inspect the busybox process:

```bash
top -Hp $(pgrep -f busybox)
```

You should see CPU usage constrained around the configured limit.

### Memory Test

Still inside the container:

```bash
python - <<'PY'
b = bytearray(700 * 1024 * 1024)
PY
```

Once the allocation crosses the 512 MiB memory limit, the kernel memory controller should kill the process and print:

```text
Killed
```

With swap enabled, the kernel may first push pages to disk. Disabling swap makes the out-of-memory behavior easier to observe.

## Cleaning Up

Exit the container shell and unmount the overlay:

```bash
exit
umount /tmp/mybox/merged
```

Then remove the temporary directories:

```bash
rm -rf /tmp/mybox
```

If any mount refuses to unmount, inspect it with:

```bash
findmnt | grep mybox
```

Then unmount nested filesystems before removing the directory.

## What This Teaches You

We built a small container-like environment using nothing but Linux kernel primitives:

- overlayFS provided the writable layer,
- cgroups enforced resource quotas,
- namespaces isolated the process environment,
- `pivot_root` moved the process into a new root filesystem.

Docker, Podman, containerd, and **Kubernetes** make these mechanisms usable at scale. They add image distribution, networking, metadata, security defaults, lifecycle management, and operational ergonomics.

But underneath all of that, the core ideas are the same.

Once you build a tiny container by hand, containers stop feeling like magic. They become a set of understandable kernel features assembled in the right order.

That understanding is useful when debugging production containers, hardening workloads, or explaining why isolation is powerful but not absolute.
