A container is not a small virtual machine. A container is a regular process launched with controlled namespaces, cgroups, filesystem mounts, and security restrictions.
flowchart TD
A[User command] --> B[Container engine]
B --> C[Image pull]
B --> D[Image unpack]
B --> E[Filesystem snapshot]
B --> F[Runtime specification]
F --> G[Low-level OCI runtime]
G --> H[Linux process]
H --> I[Namespaces]
H --> J[cgroups]
H --> K[Capabilities]
H --> L[seccomp / AppArmor / SELinux]
| Mechanism | What it controls | Container example |
|---|---|---|
| PID namespace | Process IDs | The container sees its own process tree |
| Network namespace | Network interfaces and routes | The container gets its own network stack |
| Mount namespace | Filesystem view | The container sees an image-based root filesystem |
| UTS namespace | Hostname | The container can have its own hostname |
| User namespace | User and group mapping | Root inside the container may map to non-root outside |
| cgroups | Resource limits and accounting | CPU and memory limits |
| capabilities | Fine-grained root privileges | Allow network bind but deny kernel module loading |
| seccomp | System call filtering | Block dangerous or unnecessary syscalls |
Different tools emphasize different layers of the stack.
flowchart LR
A[Developer workflow] --> B[Docker]
A --> C[Podman]
D[HPC / scientific workflow] --> E[Apptainer / Singularity]
F[Runtime plumbing] --> G[containerd]
G --> H[runc or crun]
H --> I[Linux kernel]
This lecture uses four tools to explain the ecosystem:
| Tool | Main role | Best mental model |
|---|---|---|
| Docker | Developer-friendly container platform | Convenient default container workflow |
| Podman | Daemonless, often rootless container engine | Docker-like workflow without a central Docker daemon |
| Apptainer / Singularity | HPC and scientific container runtime | Reproducible scientific environment as a portable artifact |
| containerd | Container runtime service | Lower-level runtime layer used by larger systems |
1
physical server -> hypervisor -> VM -> Linux OS -> container engine -> containers
flowchart LR
A[docker CLI] --> B[dockerd]
B --> C[containerd]
C --> D[containerd-shim]
D --> E[runc]
E --> F[container process]
Docker is not insecure by nature, but Docker makes it very easy to forget which layer is trusted. That is where the trouble starts.
Buildah and Skopeo
1
2
3
4
5
# Docker
docker run --rm alpine echo hello
# Podman
podman run --rm alpine echo hello
Simplified Podman path:
flowchart LR
A[podman CLI] --> B[libpod]
B --> C[containers/storage]
B --> D[containers/image]
B --> E[OCI runtime: crun or runc]
E --> F[container process]
| Component | Role |
|---|---|
containers/image | Pull, push, copy, and inspect container images |
containers/storage | Manage local container image/layer storage |
crun or runc | Low-level OCI runtime |
| Buildah | Build container images |
| Skopeo | Inspect and transfer images without necessarily running them |
1
root inside container -> regular user outside container
1
2
3
podman info
podman unshare cat /proc/self/uid_map
podman run --rm alpine id
Traditional container platforms were heavily shaped by web-enabled cloud applications and microservice architectures.
That model is not always suitable for scientific and HPC communities.
Scientific users often need:
Before containers, scientists often exchanged:
Today, scientists increasingly exchange workflows. The workflow includes code, tools, libraries, runtime assumptions, and sometimes model/data processing pipelines.
Containers help package those assumptions into a runnable environment.
Challenges with traditional Docker on shared HPC systems:
This is why many HPC centers prefer Apptainer/Singularity.
| Feature | Docker/Podman image | Apptainer SIF image |
|---|---|---|
| Typical storage | Layered local image store | Single image file |
| Common use | Services and applications | Scientific workflows |
| Execution model | Often isolated service process | Often user-preserving execution |
| Distribution | Registry-first | File or registry |
| HPC fit | Possible but administratively complicated | Designed for HPC expectations |
containerd is a container runtime service.runc Docker and Podman are tools humans use directly. containerd is runtime plumbing that platforms use underneath.
Several command-line tools can interact with containerd-related systems.
| Tool | Audience | Purpose |
|---|---|---|
ctr | Runtime developers and administrators | Raw containerd client |
nerdctl | Humans who want Docker-like commands | User-friendly CLI for containerd |
crictl | Kubernetes/node administrators | Debug CRI-compatible runtimes |
| Feature | Docker | Podman | Apptainer / Singularity | containerd |
|---|---|---|---|---|
| Primary audience | Developers, DevOps | Developers, sysadmins, rootless users | HPC/scientific users | Platform/runtime systems |
| Main CLI | docker | podman | apptainer / singularity | ctr, nerdctl, crictl |
| Daemon model | Uses Docker daemon | Daemonless for common local use | No Docker-style daemon | Runtime daemon |
| Rootless support | Available | Central design goal | Central HPC expectation | Possible but not beginner-friendly |
| Image format | OCI/Docker images | OCI/Docker images | SIF, can consume Docker/OCI images | OCI images |
| Best use case | General container development | Docker-like Linux workflow without Docker daemon | Reproducible scientific/HPC workflows | Runtime layer for platforms |
| Student takeaway | Convenient on-ramp | Security-conscious Docker alternative | HPC container model | What exists beneath higher-level tools |
This demonstration installs and tests three kinds of container tooling on FABRIC Ubuntu nodes:
The demonstration intentionally uses a very small image, alpine, so that the focus is on the engine behavior rather than application complexity.
This Python snippet assumes that a FABRIC slice already exists and that the slice and fablib objects are available in the notebook.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pathlib import Path
nodes = slice.get_nodes()
private_key = fablib.get_default_slice_key()["slice_private_key_file"]
ssh_config = "/home/fabric/work/fabric_config/ssh_config"
inventory = """all:
children:
fabric_nodes:
hosts:
"""
for node in nodes:
inventory += f""" {node.get_name()}:
ansible_host: "{node.get_management_ip()}"
ansible_user: "{node.get_username()}"
ansible_ssh_private_key_file: "{private_key}"
ansible_ssh_common_args: "-F {ssh_config}"
ansible_python_interpreter: "/usr/bin/python3"
"""
Path("inventory.yml").write_text(inventory)
print(inventory)
Test the inventory:
1
ansible all -i inventory.yml -m ping
Save as install-container-engines.yml.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
---
- name: Install Podman, Apptainer, containerd, and nerdctl on FABRIC Ubuntu nodes
hosts: fabric_nodes
become: true
gather_facts: true
vars:
container_user: "{{ ansible_user | default(ansible_facts.user_id) }}"
# Update this version if a newer nerdctl release is desired.
nerdctl_version: "2.2.2"
nerdctl_arch_map:
x86_64: amd64
aarch64: arm64
nerdctl_arch: "{{ nerdctl_arch_map[ansible_architecture] | default('amd64') }}"
nerdctl_url: "https://github.com/containerd/nerdctl/releases/download/v{{ nerdctl_version }}/nerdctl-{{ nerdctl_version }}-linux-{{ nerdctl_arch }}.tar.gz"
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tasks:
- name: Install base packages, Podman, containerd, runc, and CNI plugins
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
- software-properties-common
- python3-apt
- python3-launchpadlib
- uidmap
- slirp4netns
- fuse-overlayfs
- runc
- containernetworking-plugins
- containerd
- podman
state: present
- name: Add Apptainer Ubuntu PPA
ansible.builtin.apt_repository:
repo: ppa:apptainer/ppa
state: present
update_cache: true
- name: Install Apptainer
ansible.builtin.apt:
name: apptainer
state: present
- name: Ensure containerd configuration directory exists
ansible.builtin.file:
path: /etc/containerd
state: directory
mode: "0755"
- name: Generate default containerd configuration if missing
ansible.builtin.shell: |
containerd config default > /etc/containerd/config.toml
args:
creates: /etc/containerd/config.toml
notify: Restart containerd
- name: Use systemd cgroup driver in containerd config
ansible.builtin.replace:
path: /etc/containerd/config.toml
regexp: "SystemdCgroup = false"
replace: "SystemdCgroup = true"
notify: Restart containerd
- name: Enable and start containerd
ansible.builtin.systemd:
name: containerd
enabled: true
state: started
- name: Install nerdctl CLI for containerd
ansible.builtin.unarchive:
src: "{{ nerdctl_url }}"
dest: /usr/local/bin
remote_src: true
creates: /usr/local/bin/nerdctl
- name: Ensure nerdctl is executable
ansible.builtin.file:
path: /usr/local/bin/nerdctl
mode: "0755"
- name: Configure subuid for rootless Podman
ansible.builtin.lineinfile:
path: /etc/subuid
regexp: "^{{ container_user }}:"
line: "{{ container_user }}:100000:65536"
create: true
- name: Configure subgid for rootless Podman
ansible.builtin.lineinfile:
path: /etc/subgid
regexp: "^{{ container_user }}:"
line: "{{ container_user }}:100000:65536"
create: true
- name: Enable lingering for rootless user services
ansible.builtin.command: "loginctl enable-linger {{ container_user }}"
changed_when: false
failed_when: false
- name: Verify Podman
ansible.builtin.command: podman --version
register: podman_version
changed_when: false
- name: Verify Apptainer
ansible.builtin.command: apptainer --version
register: apptainer_version
changed_when: false
- name: Verify containerd
ansible.builtin.command: containerd --version
register: containerd_version
changed_when: false
- name: Verify nerdctl
ansible.builtin.command: nerdctl --version
register: nerdctl_version_output
changed_when: false
- name: Show installed versions
ansible.builtin.debug:
msg:
- "{{ podman_version.stdout }}"
- "{{ apptainer_version.stdout }}"
- "{{ containerd_version.stdout }}"
- "{{ nerdctl_version_output.stdout }}"
handlers:
- name: Restart containerd
ansible.builtin.systemd:
name: containerd
state: restarted
Run it:
1
ansible-playbook -i inventory.yml install-container-engines.yml
Save as demo-container-engines.yml.
This playbook runs all demos with become: true for reliability in non-interactive FABRIC/Ansible sessions. In a live terminal, students can also run Podman manually as a regular user to inspect rootless behavior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
---
- name: Demonstrate Podman, Apptainer, and containerd on FABRIC nodes
hosts: fabric_nodes
become: true
gather_facts: false
tasks:
- name: Podman demo
ansible.builtin.command: >
podman run --rm --net=host docker.io/library/alpine:3.20
sh -c 'echo "Podman demo"; echo "hostname=$(hostname)"; echo "uid=$(id -u) gid=$(id -g)"; cat /etc/alpine-release'
register: podman_demo
changed_when: false
- name: Show Podman output
ansible.builtin.debug:
var: podman_demo.stdout_lines
- name: Apptainer demo using a Docker/OCI image
ansible.builtin.command: >
apptainer exec docker://alpine:3.20
sh -c 'echo "Apptainer demo"; echo "hostname=$(hostname)"; echo "uid=$(id -u) gid=$(id -g)"; cat /etc/alpine-release'
register: apptainer_demo
changed_when: false
- name: Show Apptainer output
ansible.builtin.debug:
var: apptainer_demo.stdout_lines
- name: Pull Alpine into containerd content store using ctr
ansible.builtin.command: ctr images pull docker.io/library/alpine:3.20
register: ctr_pull
changed_when: false
- name: Run Alpine directly with containerd ctr
ansible.builtin.command: >
ctr run --rm docker.io/library/alpine:3.20 ctr-demo
sh -c 'echo "containerd ctr demo"; echo "hostname=$(hostname)"; echo "uid=$(id -u) gid=$(id -g)"; cat /etc/alpine-release'
register: ctr_demo
changed_when: false
- name: Show ctr output
ansible.builtin.debug:
var: ctr_demo.stdout_lines
- name: Run Alpine with nerdctl
ansible.builtin.command: >
nerdctl run --rm --net=host docker.io/library/alpine:3.20
sh -c 'echo "nerdctl demo"; echo "hostname=$(hostname)"; echo "uid=$(id -u) gid=$(id -g)"; cat /etc/alpine-release'
register: nerdctl_demo
changed_when: false
- name: Show nerdctl output
ansible.builtin.debug:
var: nerdctl_demo.stdout_lines
- name: List Podman images
ansible.builtin.command: podman images
register: podman_images
changed_when: false
- name: List containerd images
ansible.builtin.command: ctr images ls
register: ctr_images
changed_when: false
- name: Compare image stores
ansible.builtin.debug:
msg:
- "Podman images:"
- "{{ podman_images.stdout_lines }}"
- "containerd images:"
- "{{ ctr_images.stdout_lines }}"
Run it:
1
ansible-playbook -i inventory.yml demo-container-engines.yml
After the Ansible demo, you can SSH into one FABRIC node and run these commands manually.
Podman:
1
2
3
podman run --rm alpine:3.20 sh -c 'hostname; id; cat /etc/alpine-release'
podman images
podman info
Apptainer:
1
2
3
apptainer pull alpine.sif docker://alpine:3.20
apptainer exec alpine.sif sh -c 'hostname; id; cat /etc/os-release'
ls -lh alpine.sif
containerd:
1
2
3
4
5
sudo systemctl status containerd --no-pager
sudo ctr namespaces ls
sudo ctr images pull docker.io/library/alpine:3.20
sudo ctr images ls
sudo ctr run --rm docker.io/library/alpine:3.20 manual-ctr-demo cat /etc/alpine-release
nerdctl:
1
2
sudo nerdctl run --rm --net=host alpine:3.20 cat /etc/alpine-release
sudo nerdctl images