The containerized process backend runs blockyard as PID 1 in a
Docker container, with bubblewrap and R pre-installed, and no
Docker socket mount. A compromised blockyard server is confined to
the container — it has no root-equivalent access to the host.
This is the recommended mode for multi-tenant deployments where you
don’t want to expose /var/run/docker.sock and don’t need Docker’s
per-worker bridge networks.
For the native (bare-metal) variant, see Process Backend (Native). For the security trade-offs between the Docker and process backends, see Backend Security.
The image#
ghcr.io/cynkra/blockyard-process:<version> ships the blockyard
binary compiled with -tags 'minimal,process_backend' (no Docker SDK
in the dep graph), plus:
ubuntu:24.04base- R (the current release at image build time), installed via
rigso operators can swap R versions at deploy time via the extras hook — see below - Runtime shared libraries commonly needed by R packages
(libcurl, libssl, libxml2, libcairo, libpango, libpq, libmariadb,
libsqlite3, unixodbc, libzstd, …). No compiler toolchain and no
-devheaders; extra libraries are added via the extras hook. bubblewrap- The compiled bwrap seccomp profile at
/etc/blockyard/seccomp.bpf - The outer-container seccomp profile at
/etc/blockyard/seccomp.json(for extraction to the host)
Extending the image — the extras hook#
The image runs /etc/blockyard/extras.sh as root before starting
the blockyard server. A no-op default is baked in; operators
override it by bind-mounting their own script.
Use the hook to:
- install additional system libraries for R packages your bundles
need (libgdal for
sf/terra, libpoppler forpdftools, …) - pin or add R versions via
rig(e.g.rig add 4.4.3) - add custom apt sources and GPG keys
- drop
.netrcor credentials files into/root
Example:
#!/bin/sh
# extras.sh
set -e
# Pin a specific R version instead of the baked-in release
rig add 4.4.3
rig default 4.4.3
# Spatial libraries for sf / terra
apt-get update
apt-get install -y --no-install-recommends \
libgdal34t64 libgeos-c1t64 libproj25 libudunits2-0
rm -rf /var/lib/apt/lists/*See docker/extras.example.sh in the blockyard repository for a
fuller example with commented blocks for common R ecosystem
extras.
Mount patterns#
Docker / docker-compose:
services:
blockyard:
image: ghcr.io/cynkra/blockyard-process:1.2.3
volumes:
- ./extras.sh:/etc/blockyard/extras.sh:roKubernetes: create a ConfigMap from the script and mount a
single items entry at the target path:
apiVersion: v1
kind: ConfigMap
metadata:
name: blockyard-extras
data:
extras.sh: |
#!/bin/sh
set -e
apt-get update
apt-get install -y --no-install-recommends libgdal34t64
rm -rf /var/lib/apt/lists/*
---
# in the Deployment's pod spec:
volumeMounts:
- name: extras
mountPath: /etc/blockyard/extras.sh
subPath: extras.sh
readOnly: true
volumes:
- name: extras
configMap:
name: blockyard-extras
defaultMode: 0755Failure semantics#
The entrypoint shim runs set -e before executing the extras
script. A non-zero exit aborts container startup with a clear
error visible in docker logs / kubectl logs. Typos and missing
packages surface immediately instead of turning into mysterious
dyn.load() failures at first user session.
Scan drift caveat#
The Trivy scan in the blockyard CI publishes findings for the built image. Any packages or R versions added at startup via the extras hook are not covered by that scan — the operator owns the CVE picture of whatever they layer on. Operators who need the scan to reflect reality should bake their own image instead:
FROM ghcr.io/cynkra/blockyard-process:1.2.3
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgdal34t64 \
&& rm -rf /var/lib/apt/lists/*Baked-in packages are scanned by whatever image-scanning pipeline the operator runs on their own registry.
Airgapped and network-restricted deploys#
The default extras hook is a no-op, so the out-of-the-box image
starts with no network access required. But any extras script
that calls apt-get update, rig add, or downloads anything
over HTTP needs outbound connectivity at container start. For
airgapped deploys, bake what you need into a derived image
instead of using the runtime hook — the FROM ghcr.io/cynkra/blockyard-process:<v> pattern above is the
airgap-friendly path.
Why the outer seccomp profile is needed#
Docker’s default seccomp profile blocks the clone/clone3/unshare/
setns syscalls with the CLONE_NEWUSER flag unless the process has
CAP_SYS_ADMIN. When bwrap inside the blockyard container tries to
unshare(CLONE_NEWUSER) to create a worker sandbox, the kernel rejects
the call with EPERM and the worker fails to spawn.
Blockyard ships a custom seccomp profile that relaxes only the user-namespace-creation syscalls. No other capability gates are relaxed; no additional syscalls are added. The rest of Docker’s default restrictions stay in place.
Operators must pass this profile to the outer container via
--security-opt seccomp=<path>. Docker reads the profile from the
host, not from inside the container — so you need a copy on the host
before the container starts.
Extracting the profile#
Three options:
Option 1 — docker run --entrypoint cat (no local blockyard
binary required):
docker run --rm --entrypoint cat \
ghcr.io/cynkra/blockyard-process:1.2.3 \
/etc/blockyard/seccomp.json \
> /etc/blockyard/seccomp.jsonThe --entrypoint cat override is required because the image’s
default entrypoint is blockyard --config ...; without it the cat
would end up as an argument to blockyard.
Option 2 — by admin install-seccomp
(if you have the by CLI installed):
sudo by admin install-seccomp --target /etc/blockyard/seccomp.jsonThe profile is embedded in the by binary via //go:embed, so no
network access or running blockyard server is required.
Option 3 — download from GitHub Releases:
VERSION=1.2.3
sudo curl -fsSL -o /etc/blockyard/seccomp.json \
"https://github.com/cynkra/blockyard/releases/download/v${VERSION}/blockyard-outer.json"Docker Compose example#
services:
blockyard:
image: ghcr.io/cynkra/blockyard-process:1.2.3
security_opt:
- seccomp=/etc/blockyard/seccomp.json
volumes:
- blockyard-data:/var/lib/blockyard
- ./blockyard.toml:/etc/blockyard/blockyard.toml:ro
environment:
- BLOCKYARD_REDIS_URL=redis://redis:6379
networks:
- state
- default
ports:
- "8080:8080"
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- state
# Redis is only reachable from blockyard, not from workers.
# Expose no host port.
caddy:
image: caddy:2
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
ports:
- "80:80"
- "443:443"
networks:
- default
volumes:
blockyard-data:
redis-data:
networks:
state:
internal: true
default:Note the lack of:
--privilegedcap_add/var/run/docker.sockmount
The container needs only the custom seccomp profile. bubblewrap
inside creates user-namespaced worker sandboxes without any
additional host privileges.
Egress firewall (containerized mode)#
The iptables owner-match pattern from
Backend Security
does not work unchanged here: the outer container has its own UID
namespace, and worker processes appear as the container’s own UID
(typically root) from the host’s perspective, so host-side
--gid-owner 65534 rules will not fire.
Two options:
Run iptables rules inside the container. The
blockyard-processimage does not shipiptables, so this is not a drop-in option. Use the everything image (ghcr.io/cynkra/blockyard:<v>) if you need this.Use Docker network segmentation (recommended). Put Redis, the vault, and the database on an
internal: truenetwork that the blockyard container joins, and put worker-egress-sensitive services on a separate network workers cannot reach. Cleaner than iptables but requires deliberate service topology — the Docker Compose example above shows the pattern.
Blockyard’s preflight runs the same worker-egress probe in containerized mode. Review the startup logs for warnings about reachable internal services.
Rootless containers#
Running blockyard as a non-root user inside a container (or on k8s
with a runAsNonRoot: true pod spec) changes the egress isolation
picture. The six-layer model from
backends.md
reduces to:
Layers 1–5 (filesystem, PID, capabilities, seccomp, in-sandbox UIDs) hold regardless.
Layer 6 via
-m owneris unavailable. The fork+setuid path that produces per-worker host kuids requires CAP_SETUID.Layer 6 via cgroup-v2 delegation is available only if the container runtime grants a delegated cgroup subtree inside the container (not default). In that case, install
iptables -m cgroup --path <cgpath>/workersrules on the host or in a network-admin sidecar.AppArmor on Ubuntu 23.10+: extract and load the profile on the host (not inside the container) so it applies to the container’s blockyard process:
docker run --rm --entrypoint cat \ ghcr.io/cynkra/blockyard-process:${VERSION} \ /etc/blockyard/apparmor/blockyard | sudo tee /etc/apparmor.d/blockyard sudo apparmor_parser -r /etc/apparmor.d/blockyardThe profile attaches by path (
/usr/{bin,local/bin}/blockyard), so the container’s blockyard binary needs to match one of those paths for enforcement to apply.
For deployments that need per-worker egress isolation but land on a rootless-container surface without cgroup delegation, the Docker backend is the supported path — it gives each worker its own network namespace and per-worker bridge, independent of host iptables mechanics.
Rolling updates in containerized mode#
by admin update returns 501 Not Implemented when blockyard runs
as PID 1 in a container. The process orchestrator’s fork+exec model
requires the old and new blockyard to run as sibling processes under
a parent that survives the cutover — killing PID 1 stops the
container regardless of child process tricks.
For containerized rolling updates, use your container runtime’s update mechanism:
Docker Compose:
# Edit docker-compose.yml: update image tag to blockyard-process:1.2.4
docker compose pull blockyard
docker compose up -d blockyardKubernetes:
kubectl set image deployment/blockyard \
blockyard=ghcr.io/cynkra/blockyard-process:1.2.4Nomad:
nomad job run blockyard-1.2.4.nomadAll three give you rolling-update semantics via the runtime’s own cutover machinery (health checks, graceful shutdown, session draining), which is more battle-tested than blockyard’s fork+exec path.
Limitations#
Same as native mode, plus:
- No
by admin update/by admin rollback. Use the container runtime. - Egress firewall requires either the everything image or network segmentation.