Containers, Caches & Baked Images
Run commands inside content-addressed project Dockerfiles, hydrate lockfile-keyed dependency caches via reflink and S3, and bake fast-boot AMIs.
Fast remote execution comes down to eliminating repeated setup work across slots and ephemeral VMs. spotexec provides three composable layers:
- Project containers (
[container]) — pin exact Node, Python, Go, Rust, Bun, or JVM toolchains in a committed Dockerfile built once and cached in ECR. - Lockfile-keyed dependency caches (
[deps]) — restorenode_modules,.venv, or build caches instantly via copy-on-writereflinklocally and private S3 archives across Spot VMs. - Baked AMIs (
spotexec image bake) — snapshot your host bootstrap so fresh Spot VMs come online in bare EC2 boot time.
Project Containers#
With [container] configured in .spotexec.toml, both dependency installation and every sx command run inside a Docker container on the runner. The runner host itself only needs Docker (installed automatically on AWS Spot VMs).
Scaffold, Customize, and Build#
spotexec container init # detects stack, writes .spotexec/Dockerfile, updates .spotexec.toml
$EDITOR .spotexec/Dockerfile # add system libraries (e.g. libvips, playwright deps, protobuf)
spotexec container build -r aws # build remotely now and push to private ECR on AWS
sx pnpm test # runs inside the pinned imagespotexec container init inspects your repository manifests and writes a pinned Dockerfile:
| Detected Stack | Generated Base Image |
|---|---|
Node.js (npm, pnpm, yarn) | node:<major>-bookworm (version from .nvmrc, .node-version, or engines.node; pins pnpm via packageManager) |
| Bun | oven/bun:1 |
Python with uv | ghcr.io/astral-sh/uv:python<x.y>-bookworm (version from .python-version) |
| Python | python:<x.y>-bookworm |
| Go | golang:<x.y>-bookworm (version from go.mod) |
| Rust | rust:<version>-bookworm (from rust-toolchain.toml, plus clippy and rustfmt) |
| Maven / Gradle | eclipse-temurin:21-jdk (adds maven if ./mvnw is absent) |
| Fallback | ubuntu:24.04 with build-essential |
Dockerfile Authoring Rules#
- Install tools, never source code. Your worktree is bind-mounted at runtime, and dependencies (
node_modules,.venv) are hydrated from[deps]. Do notCOPY . .in.spotexec/Dockerfile. - Install system-wide (e.g.
/usr/local). Commands execute as the non-root runner user withHOME=/home/spotexecmounted per slot so tool caches (~/.cache, Go build cache, Cargo registry) persist across runs. Anything installed into/rootor baked into/home/spotexecinside the image will be hidden by the slot mount. - Content-addressed rebuilds.
spotexechashes the Dockerfile and its.dockerignore-filtered build context. The image is rebuilt only when that hash changes, pushed to a privatespotexec/<repo-id>ECR repository (retaining the 10 newest tags), and pulled by future Spot VMs without rebuilding.
[container]
dockerfile = ".spotexec/Dockerfile" # or pull an existing image directly with: image = "node:22-bookworm"
# context = "." # defaults to the Dockerfile's directory
# options = ["--shm-size=1g"] # extra docker run flags (useful for Chromium / Playwright)
# user = "runner" # "runner" (default; preserves file ownership) or "root"
# timeout = "20m" # image build/pull timeoutLockfile-Keyed Dependency Cache#
Configuring [deps] in .spotexec.toml avoids running pnpm install, uv sync, or npm ci from scratch on every slot or fresh Spot instance:
[deps]
lockfiles = ["pnpm-lock.yaml"]
install = "pnpm install --frozen-lockfile"
cache_dirs = ["node_modules", "packages/*/node_modules"]
hydrate = "auto" # auto (reflink, else copy) | reflink | copy | hardlinkHow Cache Keys & Hydration Work#
Each dependency archive is keyed by a cryptographic digest of:
- Lockfile contents + the
installcommand +cache_dirslist; - Runner OS and CPU architecture (
linux/amd64,linux/arm64, etc.); - Inside a
[container]: the container image's immutable image digest (so native C++/Rust addons never cross Node/Python versions); - Outside a container: the runner's AMI, bootstrap scripts, and
remote_path_setup.
Hydration operates in two tiers:
- On the same runner (cross-slot): Cached directories are cloned into each slot using filesystem copy-on-write
reflinkwhere supported (XFS, Btrfs, macOS APFS), falling back to a local copy. (hardlinkis opt-in only because shared inodes allow a mutating test in Slot 1 to corrupt Slot 2'snode_modules). - Across ephemeral AWS Spot VMs: The first VM to build a cache key compresses and uploads it to a private S3 bucket (
spotexec-cache-<account>-<region>, Block Public Access enabled, 14-day lifecycle expiry). Subsequent VMs download and unpack the archive directly instead of runninginstall.
Baked AMIs (spotexec image bake)#
spotexec image bake aws # launch temporary on-demand builder, run bootstraps, snapshot AMI
spotexec image list aws # inspect active baked AMIs and bootstrap hashes
spotexec image clear aws # deregister baked AMIs and delete backing EBS snapshotsspotexec image bake boots a temporary builder instance, runs bootstrap_script, installs Docker + Buildx, pre-pulls your project's public container image (plus any extra --pull IMAGE flags), runs user_bootstrap_script, strips per-host state (SSH host keys, cloud-init state, logs, watchdog timers), and registers a private AMI.
With ami = "auto" (the default), spotexec automatically selects the newest baked AMI matching your runner's current bootstrap hash and skips first-boot package installation—cutting cold Spot VM startup close to bare EC2 boot time. Changing bootstrap_script, user_bootstrap_script, arch, or user invalidates old hashes cleanly; spotexec retains only the 2 newest bakes per runner and garbage-collects older snapshots.