Docker implemented in around 300 lines of bash ⚡️
Fork of bocker (github.com/p8952/bocker). updated & expanded for 2026.
- Prerequisites
- Example Usage
- Functionality: Currently Implemented
- 2026 Modernization
- btrfs /var/shocker setup
- Building images
- proxy / private registry
- Functionality: Not Yet Implemented
- License
- Control Groups (cgroups)
- More crazy? shocker-in-shocker
- Understanding containers from scratch
- Using shocker on a mac
The following packages are needed to run shocker.
- btrfs-progs
- curl
- python3
- iproute2
- iptables
- util-linux
- coreutils
- bash
Additionally your system will need or gets configured with the following:
- A
btrfsfilesystem mounted under/var/shocker(see below for setup help) - A network bridge called
bridge0and an IP of 10.0.0.1/24 (gets created if missing) - IP forwarding enabled in
/proc/sys/net/ipv4/ip_forward(gets created if missing) iptablesNAT and FORWARD rules forbridge0(gets created if missing)
You probably still want to run shocker in a virtual machine. shocker runs as root and among other things, makes changes to your network interfaces, routing table, and firewall rules.
We can make no guarantees that it won't trash your system.
> shocker pull alpine
Pulling layer 6a0ac1617861a677b04…
Created: img-aa090
> shocker images
IMAGE_ID SOURCE
img-aa090 alpine:latest
> shocker run alpine cat /etc/issue
Welcome to Alpine Linux 3.23
Kernel \r on \m (\l)
> shocker ps
CONTAINER_ID COMMAND
ps-58122 cat /etc/issue
> shocker logs ps-58122
Welcome to Alpine Linux 3.23
Kernel \r on \m (\l)
> shocker rm ps-58122
Removed: ps-58122
> shocker run --rm alpine curl
/bin/sh: curl: not found
> shocker run alpine apk add curl
(1/9) Installing brotli-libs (1.2.0-r0)
(2/9) Installing c-ares (1.34.6-r0)
(3/9) Installing libunistring (1.4.1-r0)
(4/9) Installing libidn2 (2.3.8-r0)
(5/9) Installing nghttp2-libs (1.69.0-r0)
(6/9) Installing libpsl (0.21.5-r3)
(7/9) Installing zstd-libs (1.5.7-r2)
(8/9) Installing libcurl (8.19.0-r0)
(9/9) Installing curl (8.19.0-r0)
Executing busybox-1.37.0-r30.trigger
OK: 13.0 MiB in 25 packages
> shocker ps
CONTAINER_ID COMMAND
ps-19998 apk add curl
> shocker commit ps-19998 img-aa090
Removed: img-aa090
Created: img-aa090
> shocker run --rm img-aa090 which curl
/usr/bin/curl
> shocker run --rm -it img-aa090
/ # ls
bin etc img.source media opt ps-19998.cmd ps-ab245.cmd run srv tmp var
dev home lib mnt proc ps-19998.log root sbin sys usr
/ # date
Fri May 22 23:54:38 UTC 2026
/ # exitdocker build†docker pulldocker imagesdocker psdocker rundocker execdocker logsdocker commitdocker rm/docker rmi- Networking
- Quota Support / CGroups
† shocker init is a docker import equivalent — it promotes a local directory into a shocker image. For a proper build workflow, see Building images below.
shocker_pull→ full rewrite for Registry v2- The original used Docker's v1 registry API (
index.docker.io/v1/), which was shut down in 2023. Shocker uses the v2 API with proper OAuth2 Bearer token auth, fetches the manifest (accepting both Docker schema v2 and OCI formats), extracts layer digests, and pulls blobs by digest.
- The original used Docker's v1 registry API (
cgroups v1 → v2 support- Most modern distros (Ubuntu 22.04+, Fedora 31+, etc.) ship with cgroups v2 unified hierarchy. The original's cgcreate/cgset/cgexec/cgdelete commands only work with v1. Shocker detects the version at startup and uses either:
- v2: writes directly to
/sys/fs/cgroup/, mapscpu.shares→cpu.weight,memory.limit_in_bytes→memory.max - v1: falls back to the original cgroup-tools commands
- UUID space expansion
- bocker used
shuf -i 42002-42254— only 252 possible values, almost guaranteed collision if you run a few containers. shocker uses 5 hex digits($RANDOM * $RANDOM % 0xFFFFF)= ~1 million values.
- bocker used
- MAC address generation
- The original's
02:42:ac:11:00$macwas only a 2-octet suffix from 3 decimal digits, producing malformed MACs. shocker derives a proper 6-octet MAC from 4 hex chars of the UUID.
- The original's
shocker_exec PIDlookup- The original's nested
ps | grep | awk | greppipeline was fragile and often broke. Replaced with a singleps -eo pid,args | awkthat matches the unshare process for the container UUID.
- The original's nested
- IP derivation — now uses the full 5-hex-digit UUID value (
16#${uuid} % 253) rather than just the last 2 hex digits, significantly reducing the chance of two containers getting the same IP - MAC address — zero-pads the UUID to 8 hex digits with
printf '%08x'then slices into 4 cleanxxoctets; fixes potential truncation with short UUID values and removes fragilesedregex shocker_pull— manifest list handling — now accepts manifest list / OCI image index media types, detects which type was returned, then resolves to the correct platform digest (matchinguname -mnormalized to OCI arch names:amd64,arm64,arm) before fetching the actual image manifest; fixes accidentally pulling layers for the wrong architectureshocker_pull— config blob fix — switched fromgrepon raw JSON topython3parsinglayers[]explicitly; the config blob lives underconfignotlayers, so the old grep was incorrectly including it and causingtarto fail on a non-tarball
- Default tag to
:latestwhen omitted - Registry v2 API with multi-arch manifest list support
SHOCKER_REGISTRYenv var for Nexus/mirror pull-through (see below)- Two-step CDN blob download to work around proxy allowlists
btrfscheck with setup instructions (see below)bridge0+ NAT auto-created on first run- cgroups v2 support with proper quoting in
_cg_exec - --mount-proc= fix for proc inside chroot
run --rmandrun -itnew optional CLI flags features working- switch DNS resolver from google to cloudflare
- check
btrfsfor non-help screens - all commands working 🎉🚀
- add
iptablesnetwork forwarding so networking out inside containers works - use host
/etc/resolv.confsince may have specific DNS resolving setup
btrfsfilesystem mounted at/var/shocker(see below)bridge0network bridge already configured (or gets created)- ip /
iptablesfor NAT (same as before) - Running as root 🤠
# 1. Install btrfs tools if needed
apt install -y btrfs-progs # Debian/Ubuntu
# 2. Create a sparse 10G image file (sparse = no real disk used until written)
truncate -s 10G /var/shocker.img
# 3. Format it
mkfs.btrfs /var/shocker.img
# 4. Mount it
mkdir -p /var/shocker
mount -o loop /var/shocker.img /var/shocker
# 5. Persist across reboots
echo '/var/shocker.img /var/shocker btrfs loop 0 0' >> /etc/fstabThe recommended approach (equivalent to docker build with an interactive layer):
# 1. start a shell in your base image, mounting your source tree
shocker run -v /my/repo:/repo -it img-base sh
# 2. inside: install deps, copy files, configure
apk add python3
mkdir -p /app && cp -r /repo/src /app
exit
# 3. find the stopped container and promote it to a named image
shocker ps
shocker commit ps-XXXXX img-myapp
# 4. verify
shocker run --rm img-myapp python3 /app/main.pyYou can see a list of shell calls for shocker run at: example-run.md
shocker init <dir> is useful for the "from scratch" case — when you've manually assembled a minimal chroot directory (e.g. via ldd + copy, as in the tutorial below) and want to import it as an image.
Use SHOCKER_REGISTRY to point at a pull-through mirror (e.g. Nexus) or a private registry:
# pull-through mirror (anonymous)
SHOCKER_REGISTRY=nexus.example.com:8080 shocker pull alpine
# private registry with Bearer auth (e.g. registry.example.com)
SHOCKER_REGISTRY=registry.example.com \
SHOCKER_REGISTRY_USER=myuser \
SHOCKER_REGISTRY_PASS=mytoken \
shocker pull myorg/myimage:tagWhen the registry responds with a Www-Authenticate: Bearer challenge, shocker automatically
fetches a JWT from the realm URL using the supplied credentials.
- Data Volume Containers
- Data Volumes
- Port Forwarding
Copyright (C) 2026 Tracey Jaquith
Copyright (C) 2015 Peter Wilmott
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
We auto-detect cgroups v2 vs v1 and work with both.
We limit each container by default to:
- 512MB of memory
- you can change this by setting env var
SHOCKER_MEM_LIMITto something like 1024 (for 1GB)
- 1024 cpu shares
- you can change this by setting env var
SHOCKER_CPU_SHARE - it's a soft limit - only kicks in when processes are competing for CPU
- you can change this by setting env var
Essentially you can share mount the /var/shocker dir and btrfs volume
(kind of like sharing /var/lib/containers with podman).
Here's an example, adjust accordingly:
# assumes you cloned repo to $HOME/shocker
shocker pull debian:trixie
shocker run -v $HOME/shocker:/app -v /var/shocker --rm -it debian:trixie sh
# NOTE: for `apt update` or similar you might find your `/tmp` needs sticky bit set, eg:
chmod 1777 /tmp
apt-get update -yqq
apt-get install -yqq btrfs-progs curl python3 iproute2 iptables util-linux coreutils bash
/app/shocker pull alpine
/app/shocker run --rm -it alpine sh -c 'cat /etc/issue'
# Welcome to Alpine Linux 3.23
#Kernel \r on \m (\l)
exitcurious to run just chroot?
# setup a chroot dir (*everything* has to get copied in)
mkdir /tmp/myimg
echo hello > /tmp/myimg/hello.txt
# see what shared libs each binary needs (linux-vdso is virtual — kernel provides it, no file to copy)
BINS=(/bin/sh /bin/cat /bin/echo)
for b in "${BINS[@]}"; do ldd "$b"; done
# mirror each lib's directory structure into the chroot, then copy
for b in "${BINS[@]}"; do ldd "$b" | grep -Eo '/[^ ]+'; done | sort -u | \
while IFS= read -r lib; do
mkdir -p "/tmp/myimg$(dirname "$lib")"
cp "$lib" "/tmp/myimg$lib"
done
# copy the binaries themselves
mkdir -p /tmp/myimg/bin
cp "${BINS[@]}" /tmp/myimg/bin/
# mount proc so the shell isn't blind, then drop in
mkdir -p /tmp/myimg/proc
mount -t proc proc /tmp/myimg/proc
chroot /tmp/myimg /bin/sh
# exit to leave
# cleanup
umount /tmp/myimg/proc...
chroot /tmp/myimg /bin/sh
mount: (hint) your fstab has been modified, but systemd still uses
the old version; use 'systemctl daemon-reload' to reload.
# cat hello.txt
hello
# echo hai
hai
# exitfind /tmp/myimg -type f |sort
/tmp/myimg/bin/cat
/tmp/myimg/bin/echo
/tmp/myimg/bin/sh
/tmp/myimg/hello.txt
/tmp/myimg/lib64/ld-linux-x86-64.so.2
/tmp/myimg/lib/x86_64-linux-gnu/libc.so.6this will have file/dir isolation, but not process isolation (eg: ps aux shows host processes)
# pick some shocker image you have pulled and update this line:
IMG=/var/shocker/img-0a690
mount -t proc proc $IMG/proc
chroot $IMG /bin/sh
# play around ^, exit, then cleanup:
umount $IMG/procthis has process isolation and more, adding on unshare
-f --fork dont hold namespace open after your shell exits
-m --mount make all mounts inside private to your namespace; when exits, auto unmounts & tears down
-p --pid give chroot own PID namespace; starts w/ pid 1 and cant see host pids
unshare -fump chroot /var/shocker/img-85e96 /bin/sh
# optional, for top, ps faux inside, you can do:
# mount -t proc proc /procmacOS doesn't have the Linux kernel features shocker needs, but you can run it inside a Podman VM (which is a real Linux VM, not a container):
# one-time setup
brew install podman
podman machine init
podman machine start
# ssh into the VM and become root
podman machine ssh
sudo su -
# install shocker dependencies (Fedora CoreOS uses dnf)
dnf install -y btrfs-progs curl python3 iptables util-linux coreutils bash
# download shocker
curl https://raw.githubusercontent.com/traceypooh/shocker/refs/heads/master/shocker > shocker
chmod +x shocker
# set up btrfs image file and mount it (see linux setup above)
truncate -s 10G /var/shocker.img
mkfs.btrfs /var/shocker.img
mkdir -p /var/shocker
mount -o loop /var/shocker.img /var/shocker
# pull and run
./shocker pull alpine
./shocker run --rm -it alpineThe podman VM persists between podman machine stop / podman machine start but the btrfs mount does not survive reboots — re-run the mount line after restarting.