shocker

Docker implemented in around 300 lines of bash ⚡️

View on GitHub

shocker

Docker implemented in around 300 lines of bash ⚡️

Fork of bocker (github.com/p8952/bocker). updated & expanded for 2026.

Prerequisites

The following packages are needed to run shocker.

Additionally your system will need or gets configured with the following:

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.

Example Usage

> 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
/ # exit

Functionality: Currently Implemented

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.

2026 Modernization

What broke in bocker and what shocker fixes

Second pass of fixes/improvements

Third pass of fixes

Fourth pass of fixes/improvements

Fifth pass of fixes/improvements

Things that still require the same host setup as bocker

btrfs /var/shocker setup

# 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/fstab

Building images

The 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.py

You 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.

proxy / private registry

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:tag

When the registry responds with a Www-Authenticate: Bearer challenge, shocker automatically fetches a JWT from the realm URL using the supplied credentials.

Functionality: Not Yet Implemented

License

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/.

Control Groups (cgroups)

We auto-detect cgroups v2 vs v1 and work with both.

We limit each container by default to:

More crazy? shocker-in-shocker

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)
  exit

Understanding containers from scratch

curious 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

example run:

...
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
# exit

example layout:

find /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.6

direct chroot into a shocker image

this 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/proc

direct chroot with unshare

this 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 /proc

Using shocker on a mac

macOS 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 alpine

The 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.