For why the networking modes behave the way they do, read Networking. For the trust model and the tradeoffs behind these defaults, read Security.
One image serves everything — API, maintenance commands, migrations.
It's published to ghcr.io/czpython/drukbox on every release; build
docker build -t ghcr.io/czpython/drukbox . only to run a local change. The
secrets proxy image, ghcr.io/czpython/drukbox/proxy, is published beside it
with the same tags.
IMAGE=ghcr.io/czpython/drukbox:latest
# API (port 8780; /healthz for liveness probes)
docker run --rm -p 8780:8780 --env-file drukbox.env "$IMAGE"
# Migrations (one-off, before first start and on upgrades)
docker run --rm --env-file drukbox.env "$IMAGE" .venv/bin/alembic upgrade head
# Maintenance (cron, e.g. every 10-15 min)
docker run --rm --env-file drukbox.env "$IMAGE" .venv/bin/python -m janitor
docker run --rm --env-file drukbox.env "$IMAGE" .venv/bin/python -m hosts.poolThe janitor reaps expired and orphaned hosts, marks abandoned template
builds failed, keeps failed builds for diagnosis, and deletes failed or
unused templates. The pool maintainer
pre-provisions warm hosts per provider and only does anything when at
least one provider has a warm target (POOL_SIZES / POOL_SIZE).
Schedule both under your cron infrastructure (k8s CronJob,
systemd timer) from the same image and env file.
Use Postgres in production (postgresql+psycopg://...). SQLite
(sqlite+aiosqlite:///./drukbox.db) is for single-process demos and
local development; the pool maintainer is safe under SQLite only with
a single runner.
The API binds all interfaces by default. When only loopback callers
reach it (host-networked, co-located client), set UVICORN_HOST=127.0.0.1
to keep the credential-holding control plane off other interfaces.
| Provider | Sandboxes | Where |
|---|---|---|
exe |
exe.dev VMs | Remote |
aws |
EC2 instances | Remote |
hetzner |
Hetzner Cloud VMs | Remote |
exoscale |
Exoscale VMs | Remote |
docker |
Containers (Local sandboxes with Docker) | Local, no external account |
docker-sbx |
microVMs (Local microVMs with Docker Sandboxes) | Local |
DEFAULT_HOST_PROVIDER selects the provider for POST /hosts (default
exe). Set the matching provider variables below. The image contains
all provider extras.
The docker provider runs each sandbox as a local container with sshd,
so you can try drukbox with no cloud account or API token. The published
image includes the Docker CLI. Set DEFAULT_HOST_PROVIDER=docker,
TAILSCALE_ENABLED=false, and UVICORN_HOST=127.0.0.1 in drukbox.env,
then run:
# Linux: match the host socket's group. macOS: use group 0 (see below).
docker run --rm --network host \
--group-add "$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo 0)" \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--env-file drukbox.env \
ghcr.io/czpython/drukbox:latestOn macOS, use --group-add 0 instead: Docker Desktop mounts the socket into
the container as root:root mode 0660, so only group 0 grants access — the
host socket's own gid is irrelevant.
Host networking is required because Docker sandboxes publish SSH on the
host's 127.0.0.1. The loopback Uvicorn binding makes the API reachable
only from that host. Do not combine this mode with the generic
-p 8780:8780 invocation above. On macOS, if sandbox SSH is
unreachable, enable host networking in Docker Desktop's settings.
The sandbox image (DOCKER_DEFAULT_IMAGE, default
ghcr.io/czpython/drukbox/sandbox:latest) is pulled on first provision.
To customize it, build images/local/ and point
DOCKER_DEFAULT_IMAGE at your tag.
Containers publish sshd on a random 127.0.0.1 port and are reachable
only from the host that runs drukbox; the per-host key is the auth
boundary. Tailscale is not supported — a local container has no path
onto the tailnet, so docker hosts stay local under a tailnet-mode
service: no join, no internal_ssh_host, the published port is the only
path. One drukbox can serve tailnet VMs and local containers side by
side.
This provider is for local development and demos, not production: it talks to the host's Docker daemon, and granting drukbox access to that socket is host-root-equivalent. Do not expose a docker-backed drukbox to untrusted callers.
Janitor and pool one-off containers using the Docker provider need the
same socket mount and socket-GID supplemental group. DOCKER_HOST remains available when the daemon is remote or
rootless instead of exposed through /var/run/docker.sock.
The docker-sbx provider runs each sandbox as a
Docker Sandboxes microVM. Each
microVM has its own kernel, its own filesystem, and its own Docker
daemon. The sandboxd network policy controls the egress. This provider
is local to the drukbox machine, the same as the docker provider. It
does not support Tailscale.
Prepare the host fully before drukbox starts. drukbox only connects to the host:
- Install Docker Engine and
docker-sbx. Ubuntu 24.04+ with KVM is necessary:/dev/kvmmust exist, and the service user must be in thekvmgroup. - Sign in one time with
sbx login. Headless hosts use a device-code flow. - Start the daemon:
sbx daemon start -d --policy balanced.
Docker documents sbx as a tool for the daemon owner's own user on the
host. Thus the simplest deployment runs drukbox directly on the host, as
the same user:
uv run uvicorn api.app:app --host 127.0.0.1 --port 8780In this mode, no mounts and no extra variables are necessary. The CLI finds the daemon socket automatically.
drukbox can also run as a container adjacent to the daemon. Docker does
not document this mode; drukbox uses the CLI's own daemon-endpoint
variable, DOCKER_SANDBOXES_API. Mount the daemon socket, the sbx
binary of the host (then the CLI version and the daemon version always
agree), the CLI auth store, and the workspace root:
docker run --rm --network host \
--mount type=bind,src=$HOME/.local/state/sandboxes/sandboxes/sandboxd/sandboxd.sock,dst=/run/sandboxd.sock \
--mount type=bind,src=$(command -v sbx),dst=/usr/local/bin/sbx,readonly \
--mount type=bind,src=$HOME/.config/com.docker.sandboxes,dst=/root/.config/com.docker.sandboxes,readonly \
--mount type=bind,src=$HOME/.drukbox/sbx-workspaces,dst=$HOME/.drukbox/sbx-workspaces \
--env DOCKER_SANDBOXES_API=unix:///run/sandboxd.sock \
--env DOCKER_SBX_WORKSPACE_ROOT=$HOME/.drukbox/sbx-workspaces \
--env-file drukbox.env \
ghcr.io/czpython/drukbox:latestThe daemon reads workspace paths on its own filesystem. Thus the workspace mount must have the same path on the host and in the container. The janitor and pool containers need the same mounts and variables.
Callers reach the sandboxes through
the SSH gateway; the provider requires it. The key
for each host is the auth boundary. Sandboxes have no SERVICE_LABEL
tag, because sbx create has no label option.
The template image (DOCKER_SBX_DEFAULT_IMAGE, default
ghcr.io/czpython/drukbox/sbx-sandbox:latest) must start sshd without
environment variables. sbx create sends none. drukbox injects the key
for each host through the exec channel after the start. Build
images/sbx/ to change the template. The
images/local/ entrypoint needs boot-time environment variables and
cannot start as a sandbox template.
The daemon has its own image store and does not read local Docker images. It pulls unknown template names from a registry. For a local template, load the image into the daemon:
docker build -t drukbox/sbx-sandbox:latest images/sbx/
docker save drukbox/sbx-sandbox:latest -o /tmp/sbx-sandbox.tar
sbx template load /tmp/sbx-sandbox.tarA sandbox creation takes approximately 20 seconds with a warm template
cache, and more than 30 seconds at the first pull. Thus a warm pool
(POOL_SIZES) is useful. Each sandbox gets the explicit
DOCKER_SBX_CPUS and DOCKER_SBX_MEMORY sizes. Without them,
the daemon gives one sandbox all host CPUs and half of the host memory.
The gateway gives remote callers SSH access to the hosts of gateway
providers such as docker-sbx, whose sandboxes have no dialable sshd
of their own. Callers connect with normal SSH:
ssh -p 2222 -i <private-key> <host-name>@<gateway-address>The gateway authenticates the key against the host's stored public key,
and the username must name the same host. It then opens a session
through the provider — sbx exec for docker-sbx. The daemon stops an
idle sandbox; a connection through the gateway wakes it (approximately
6 seconds) and keeps it awake while connected. The first data can
therefore come after a short delay.
The gateway serves an interactive shell, command execution, and SFTP. It refuses scp (the legacy protocol) and port forwarding. SFTP runs the sandbox's own SFTP server over one persistent session for each SSH connection, thus repeated file operations on one connection start no new session. The session closes after a short idle period, so an inactive sandbox still sleeps.
Every session runs with HOME set to the per-host home directory
/home/<host-name>. The gateway makes this directory and moves into it
first. Thus a caller writes files there — for example a .gitconfig or
credential files — and a later command finds them by $HOME. SFTP
relative paths also resolve against this home.
The gateway is a requirement for gateway providers: POST /hosts for
docker-sbx fails without GATEWAY_SSH_HOST. Set it to the address
callers use. The response then carries the gateway coordinates:
external_ssh_host is the gateway, external_ssh_port is the gateway
port, and ssh_username is the host name. known_hosts carries the
gateway's host key. Hosts of the other providers are not affected.
Run the gateway on the machine that runs sandboxd, as the same user, and run the migrations first:
uv run python -m gateway.serverA systemd unit follows the same pattern as the API service. The gateway
reads the same drukbox.env and connects to the same database.
| Variable | Default | Purpose |
|---|---|---|
GATEWAY_SSH_HOST |
— (required) | Address of the gateway. Gateway-provider hosts advertise it; creation fails without it. |
GATEWAY_SSH_PORT |
2222 |
Port the gateway listens on and advertises. |
GATEWAY_BIND_HOST |
0.0.0.0 |
Interface the gateway server binds. |
GATEWAY_HOST_KEY_PATH |
~/.drukbox/gateway_host_key |
Private host key. The server makes one at start when the file does not exist. |
TAILSCALE_ENABLED=false (default): callers reach sandboxes over the
provider's public path. On AWS this means per-VM keypairs and the
managed security group — see
Networking.
TAILSCALE_ENABLED=true: sandboxes join your tailnet at boot and
callers connect over the overlay. Requires a Tailscale OAuth client
with auth-key write scope, and tailnet ACLs that (a) own the tags in
TAILSCALE_AUTH_TAGS and (b) permit tailscaled-SSH to the tagged
nodes.
AWS credentials come from the SDK's default chain (instance profile,
~/.aws, or env) — drukbox never plumbs them through its own
settings. The policy needs ec2:RunInstances,
ec2:TerminateInstances, ec2:DescribeInstances, ec2:CreateTags,
sts:GetCallerIdentity, plus — with Tailscale off —
ec2:ImportKeyPair, ec2:DeleteKeyPair, ec2:CreateSecurityGroup,
ec2:DescribeSecurityGroups, ec2:AuthorizeSecurityGroupIngress,
ec2:DescribeSubnets (when AWS_SUBNET_ID is set), and
ssm:GetParameter when AWS_DEFAULT_IMAGE is an SSM path.
Drukbox tags everything it creates with managed-by=<SERVICE_LABEL>,
so write permissions can be tag-scoped.
A sandbox never holds a real third-party credential. It holds a placeholder, and it sends its HTTPS through the secrets proxy. The proxy swaps the placeholder for the real credential on the way out. Two pieces run this:
- The proxy is
ghcr.io/czpython/drukbox/proxy: the officialmitmproxy/mitmproxyimage with the addondeploy/proxy/swap.pybuilt in. It listens on 8880 atSECRETS_PROXY_BIND_HOST,0.0.0.0by default, and reads the exchange address fromSECRETS_EXCHANGE_URL. A deployment on the host network sets the bind host to the one address its sandboxes dial. On a public host an open listener is an open proxy. A checkout can mount the addon into the official image instead. It terminates TLS only for the hosts that have a registered secret and tunnels every other host blind. It refuses a destination that resolves to a loopback, private, link-local, or metadata address. It makes its CA on first start and keeps it in a volume. - The exchange process runs as
python -m secrets_exchangefrom this image. The proxy asks it which hosts to terminate, and, for a request with a placeholder, for the header the upstream reads and the real credential. Its answer is the credential, so bind it where only the proxy can reach it.
services:
exchange:
image: ghcr.io/czpython/drukbox:latest
command: [".venv/bin/python", "-m", "secrets_exchange"]
env_file: drukbox.env
environment:
SECRETS_EXCHANGE_BIND_HOST: 0.0.0.0
proxy:
image: ghcr.io/czpython/drukbox/proxy:latest
environment:
SECRETS_EXCHANGE_URL: http://exchange:8781
ports:
- "8880:8880"
volumes:
- secrets-proxy-ca:/home/mitmproxy/.mitmproxy
volumes:
secrets-proxy-ca:The exchange binds 0.0.0.0 inside the compose network and publishes no
port, so only the proxy reaches it. The API reads the public certificate of
the CA from the same volume, at SECRETS_PROXY_CA_FILE, and hands it to
every sandbox with secrets:
api:
image: ghcr.io/czpython/drukbox:latest
env_file: drukbox.env
environment:
SECRETS_PROXY_URL: http://proxy.example:8880
SECRETS_PROXY_CA_FILE: /secrets-proxy-ca/mitmproxy-ca-cert.pem
volumes:
- secrets-proxy-ca:/secrets-proxy-ca:roA sandbox with secrets gets the certificate in SECRETS_PROXY_CA, base64,
and installs it at boot with update-ca-certificates. SSL_CERT_FILE,
REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, and NODE_EXTRA_CA_CERTS point curl,
Python, and Node at it. A sandbox with a github secret gets the placeholder
in GH_TOKEN, git pointed at gh, and SSH remotes rewritten to HTTPS. That
sandbox needs git and gh. The docker sandbox image has both. Keep
flow_detail at 1 or below. A
higher level prints request headers, and after the swap those carry the
real credential. Real credentials exist in three places only. They are
encrypted in Postgres, they pass through the exchange process for one
request, and they pass through the proxy for one request.
The proxy is opt in. A deployment with only docker-sbx starts none, since sbx does the swap itself. The exchange process runs there too. It fetches a fresh issuer value before the old one expires and writes it into the value file, so it needs the api's environment and its workspace root mount.
SECRETS_PROXY_URL is the proxy a sandbox sends its HTTPS through. Every
provider but docker-sbx sets HTTPS_PROXY in the sandbox to it. For local
containers the proxy listens on the Docker bridge, for example
http://172.17.0.1:8880. A tailnet or a private network uses its own address.
The proxy is the first flow that runs from a sandbox to drukbox. Every other flow runs the other way. So the network between them needs one rule for it, one address and one port, for every sandbox. On a tailnet that is a grant from the sandbox tag to the proxy host:
"hosts": { "secrets-proxy": "100.64.0.10" },
"grants": [
{ "src": ["tag:sandbox"], "dst": ["secrets-proxy"], "ip": ["tcp:8880"] }
]A sandbox then reaches that port on that host and nothing else there. A
Docker Sandboxes sandbox dials nothing. Drukbox puts each value in sbx's own
secret store for that sandbox, and sbx's proxy swaps the placeholder. The
github service is sbx's own github secret, which covers git and gh. Any
other service, and a custom entry that names a host of its own, is a custom
secret on its hosts. The value files that sbx reads live in a secrets
directory under DOCKER_SBX_WORKSPACE_ROOT, beside the workspaces and never
inside one. sbx keeps a sandbox's secrets after the sandbox is removed, so
host deletion removes every secret in the sandbox's scope and the files. The
janitor deletes an expired host the same way. Remove a sandbox through
drukbox, never with sbx rm, or its secrets stay in sbx's store until its
row expires. Do not set a global sbx secret for a destination drukbox
manages. sbx applies the global one first, and drukbox's value never reaches
the sandbox.
Give secrets to POST /hosts. Provisioning delivers the placeholders in the
sandbox's boot environment, on every provider, the same way as env. A
refreshable secret, one given with issuer, is fetched by the exchange
process on first use and kept in memory until shortly before it expires. The
exchange process must reach the issuer URL, over plain HTTP inside the
deployment or HTTPS outside it. On docker-sbx the API process
fetches it once at provisioning, since sbx holds the value. The exchange
process pushes a fresh one before it expires, and logs each push. A pool host
takes no secrets: a request with secrets always provisions a new sandbox.
curl -fsS -H "Authorization: Bearer $TOKEN" http://localhost:8780/doctor/doctor runs one read-only probe per dependency (database, active
provider, Tailscale when enabled) and reports per-check status,
latency, and a remediation hint on failures. It always returns 200 —
health is the ok field. GET /healthz is the unauthenticated
liveness probe.
For a full end-to-end check, run the black-box suite against the deployment (it provisions and destroys a real host — disposable infrastructure only):
SERVICE_URL=http://localhost:8780 SERVICE_TOKEN=... npm --prefix api-tests testCore, required:
| Variable | Purpose |
|---|---|
DATABASE_URL |
Async SQLAlchemy URL. |
SECRETS_KEY |
Comma-separated base64 32-byte keys for encrypted host secret recipes. The first key encrypts and every key decrypts. |
SERVICE_TOKENS |
Comma-separated bearer tokens accepted from trusted callers. |
Core, optional:
| Variable | Default | Purpose |
|---|---|---|
DEFAULT_HOST_PROVIDER |
exe |
Provider used when callers don't specify one. |
SERVICE_LABEL |
drukbox |
Label stamped onto provider resources (VM tags, SG tags). |
UVICORN_HOST |
0.0.0.0 |
API bind address. Set 127.0.0.1 to restrict to loopback. |
PROVISIONING_GRACE_SECONDS |
600 |
Safety TTL on in-flight hosts so the janitor reaps row + VM if the client disconnects mid-provision. Must exceed the worst-case provision duration. |
TEMPLATE_BUILD_TIMEOUT |
3600 |
Max age in seconds of an unfinished template build before the janitor marks it failed. |
TEMPLATE_FAILED_RETENTION |
86400 |
Seconds that failed template records and diagnostics remain before the janitor deletes them. |
TEMPLATE_UNUSED_TTL |
1209600 |
Seconds that an available template remains after its last use, or creation when never used. |
LEASE_DEFAULT_TTL |
86400 |
Lease TTL in seconds for hosts created without an explicit expires_at, and the extension applied by an empty POST /hosts/{id}/renew. An explicit expires_at: null at create time opts out of expiry entirely. |
IDEMPOTENCY_KEY_TTL_HOURS |
24 |
Retention period for successful Idempotency-Key mappings. |
POOL_SIZES |
{} |
Warm hosts to keep ready per provider, as JSON (e.g. {"exe": 2, "hetzner": 1}). Overrides POOL_SIZE for the providers it names. |
POOL_SIZE |
0 |
Warm hosts to keep ready for the default provider. 0 disables its pool. |
POOL_HOST_MAX_AGE_HOURS |
4 |
Max age before the janitor reaps an unclaimed pool host. |
POOL_MAX_CREATES_PER_TICK |
2 |
Upper bound on pool provisions per tick, across all providers; caps over-provision blast radius when ticks overlap. |
Secrets exchange:
| Variable | Default | Purpose |
|---|---|---|
SECRETS_PROXY_URL |
— | Proxy a sandbox sends its HTTPS through. Required to create a host with secrets on every provider but docker-sbx. |
SECRETS_PROXY_CA_FILE |
— | Path of the proxy's public CA certificate, from the proxy's volume. Required with SECRETS_PROXY_URL. |
SECRETS_EXCHANGE_BIND_HOST |
127.0.0.1 |
Interface the exchange process binds. Bind it where only the proxy can reach it. |
SECRETS_EXCHANGE_PORT |
8781 |
Port the exchange process listens on. |
Tailscale (required when TAILSCALE_ENABLED=true):
| Variable | Default | Purpose |
|---|---|---|
TAILSCALE_ENABLED |
false |
Provision hosts onto a tailnet. |
TAILSCALE_TAILNET |
— | Tailnet DNS suffix for sandbox MagicDNS hostnames. |
TAILSCALE_AUTH_TAGS |
— | Comma-separated tags applied to minted auth keys. |
TAILSCALE_OAUTH_CLIENT_ID |
— | OAuth client ID. |
TAILSCALE_OAUTH_CLIENT_SECRET |
— | OAuth client secret. |
TAILSCALE_API_TIMEOUT |
30.0 |
Timeout for Tailscale API calls. |
DEVICE_DISCOVERY_TIMEOUT_SECONDS |
180.0 |
How long provisioning waits for a sandbox to appear in the tailnet. |
exe.dev provider. exe puts --env where only a login shell reads it, so
the setup script also writes the exports to ~/.bashrc, which every bash
session on the box reads.
exe.dev provider:
| Variable | Default | Purpose |
|---|---|---|
EXE_API_TOKEN |
— (required) | Bearer token for the exe.dev exec API. |
EXE_DEFAULT_IMAGE |
— (required) | Image used when the caller omits image. |
EXE_IMAGE_REGISTRY |
— | Repository prefix for derived template images. A VM created from this registry gets --registry-auth so exe.dev can pull a private image. |
EXE_REGISTRY_USERNAME |
— | Username for the derived-template image registry. |
EXE_REGISTRY_PASSWORD |
— | Password or token for the derived-template image registry. |
EXE_API_URL |
https://exe.dev |
API base URL. |
EXE_API_TIMEOUT |
30.0 |
Timeout for exe.dev API calls. |
EXE_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
30.0 |
ssh-keyscan retry budget for a fresh exe.dev sandbox. |
EXE_SSH_USERNAME |
exedev |
In-VM user callers SSH as. |
AWS provider:
| Variable | Default | Purpose |
|---|---|---|
AWS_REGION |
— (required) | Region for the EC2 client and launches. |
AWS_DEFAULT_IMAGE |
— (required) | AMI id or SSM parameter path used when the caller omits image. |
AWS_INSTANCE_TYPE |
t3.medium |
EC2 instance type when the caller omits instance_type. |
AWS_ROOT_GB |
100 |
Root EBS volume size (gp3, encrypted) when the caller omits disk_gb. |
AWS_SUBNET_ID |
— | Optional subnet; default VPC's otherwise. |
AWS_SECURITY_GROUP_ID |
— | Pre-existing SG; unset → drukbox manages drukbox-managed. |
AWS_SSH_CIDRS |
— | SSH ingress CIDRs. Authoritative when set; unset → detected egress /32, falling back to 0.0.0.0/0. |
AWS_INSTANCE_PROFILE |
— | Optional IAM instance profile attached to sandboxes. |
AWS_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
120.0 |
ssh-keyscan retry budget for a fresh EC2 instance. |
AWS_SSH_USERNAME |
ubuntu |
In-VM user callers SSH as. |
Hetzner provider:
| Variable | Default | Purpose |
|---|---|---|
HETZNER_API_TOKEN |
— (required) | Bearer token for the Hetzner Cloud API. |
HETZNER_LOCATION |
— (required) | Location for launches, e.g. nbg1, fsn1, hel1, ash. |
HETZNER_DEFAULT_IMAGE |
ubuntu-24.04 |
Image name/id used when the caller omits image. |
HETZNER_SERVER_TYPE |
cx23 |
Server type when the caller omits instance_type, e.g. cx23, cx33. Hetzner retires older generations (e.g. cx22); a deprecated type fails provisioning with a 422. |
HETZNER_API_TIMEOUT |
30.0 |
Timeout for Hetzner API calls. |
HETZNER_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
120.0 |
ssh-keyscan retry budget for a fresh server. |
HETZNER_SSH_USERNAME |
root |
In-VM user callers SSH as. |
A fresh Hetzner server has no firewall — port 22 is open and SSH is key-only. Drukbox mints a per-VM ed25519 key in both networking modes; there is no security-group or ingress-CIDR configuration to manage.
Exoscale provider:
| Variable | Default | Purpose |
|---|---|---|
EXOSCALE_API_KEY |
— (required) | Exoscale API key ID. |
EXOSCALE_API_SECRET |
— (required) | Exoscale API secret used to sign requests. |
EXOSCALE_ZONE |
— (required) | Zone for launches, e.g. ch-gva-2, de-fra-1. |
EXOSCALE_DEFAULT_IMAGE |
Linux Ubuntu 24.04 LTS 64-bit |
Template used when the caller omits image. |
EXOSCALE_INSTANCE_TYPE |
standard.medium |
Instance type when the caller omits instance_type. |
EXOSCALE_DISK_GB |
50 |
Root disk size in GB when the caller omits disk_gb. |
EXOSCALE_API_TIMEOUT |
30.0 |
Timeout for Exoscale API calls. |
EXOSCALE_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
120.0 |
ssh-keyscan retry budget for a fresh instance. |
EXOSCALE_SSH_USERNAME |
ubuntu |
In-VM user callers SSH as. Exoscale Ubuntu templates default to ubuntu. |
The API key's IAM role must allow the compute operations list-templates and
list-instance-types in addition to the instance and SSH-key operations:
instance creation resolves the configured template and instance-type names to
IDs through those list calls.
Docker provider:
| Variable | Default | Purpose |
|---|---|---|
DOCKER_DEFAULT_IMAGE |
ghcr.io/czpython/drukbox/sandbox:latest |
Sandbox image with sshd, git, and gh; auto-pulled. Build images/local/Dockerfile to customize. |
DOCKER_SSH_USERNAME |
root |
In-container user callers SSH as. |
DOCKER_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
30.0 |
ssh-keyscan retry budget for a fresh container. |
The published image includes the Docker CLI. Mount the local daemon socket
with its supplemental group on Linux, or use DOCKER_HOST for a remote or
rootless daemon. Drukbox mints a per-VM ed25519 key and publishes sshd on a
random 127.0.0.1 port. See
Local sandboxes with Docker for the
container command and the trust caveat.
Docker Sandboxes provider:
| Variable | Default | Purpose |
|---|---|---|
DOCKER_SBX_DEFAULT_IMAGE |
ghcr.io/czpython/drukbox/sbx-sandbox:latest |
Template image that contains sshd and starts without environment variables. Build images/sbx/Dockerfile to change it. |
DOCKER_SBX_SSH_USERNAME |
root |
User in the sandbox for caller SSH access. |
DOCKER_SBX_BOOTSTRAP_SSH_TIMEOUT_SECONDS |
30.0 |
Time limit for the ssh-keyscan tries on a new sandbox. |
DOCKER_SBX_CPUS |
2 |
Number of CPUs for each sandbox. |
DOCKER_SBX_MEMORY |
2g |
Memory for each sandbox, in binary units. |
DOCKER_SBX_WORKSPACE_ROOT |
~/.drukbox/sbx-workspaces |
Directory with one temporary workspace for each sandbox, and a secrets directory with the value files sbx reads. The path must be the same for drukbox and for the daemon. |
The published image does not contain the sbx CLI. Mount the binary and
the auth store of the host, as
Local microVMs with Docker Sandboxes
shows. Set DOCKER_SANDBOXES_API to the mounted daemon socket.