Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ flowchart LR
Remove --> Start
```

## See it in action

[![EPAR demo — disposable GitHub Actions runners with Docker Sandboxes](https://img.youtube.com/vi/5xDEFpf6iZc/maxresdefault.jpg)](https://www.youtube.com/watch?v=5xDEFpf6iZc)

Watch EPAR take a GitHub Actions job from a warm runner, execute it inside a Docker Sandboxes microVM, retire the runner after the job, and bring a clean replacement back to ready state.

[Watch the demo on YouTube →](https://www.youtube.com/watch?v=5xDEFpf6iZc)

## Why EPAR

- **Put spare compute to work** — run long-running E2E, integration, and Docker-heavy CI on machines you already operate.
Expand Down
4 changes: 3 additions & 1 deletion cmd/ephemeral-action-runner/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func runInitWithOptions(opts initOptions) error {
return err
}
}
fmt.Fprintln(opts.Out, "EPAR first-run setup")
fmt.Fprintln(opts.Out, "Welcome to EPAR first-run setup wizard.")
fmt.Fprintln(opts.Out, "")
fmt.Fprintln(opts.Out, "This creates an EPAR runner configuration.")
fmt.Fprintln(opts.Out, "Before continuing, create a GitHub App with organization self-hosted runner read/write access.")
Expand Down Expand Up @@ -2059,6 +2059,8 @@ dockerSandboxes:
policyGeneration: %s
networkBaseline: open
architectureEmulation: %s
recoveryMode: exclusive-auto
recoveryQuiescenceSeconds: 60
stagingRoot: .local/cache/docker-sandboxes/staging
cpus: 4
memory: 8GiB
Expand Down
30 changes: 23 additions & 7 deletions cmd/ephemeral-action-runner/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,11 +1125,20 @@ func TestInitDockerSandboxesGeneratesDesiredImageConfigAndProvisionsTemplate(t *
if got, want := cfg.DockerSandboxes.NetworkBaseline, config.DockerSandboxesNetworkBaselineOpen; got != want {
t.Fatalf("dockerSandboxes.networkBaseline = %q, want %q", got, want)
}
if got, want := cfg.DockerSandboxes.ArchitectureEmulation, config.DockerSandboxesArchitectureEmulationBestEffort; got != want {
if got, want := cfg.DockerSandboxes.ArchitectureEmulation, config.DockerSandboxesArchitectureEmulationNativeOnly; got != want {
t.Fatalf("dockerSandboxes.architectureEmulation = %q, want wizard value %q", got, want)
}
if !strings.Contains(string(configContent), "architectureEmulation: best-effort") {
t.Fatalf("wizard config omitted best-effort architecture emulation:\n%s", configContent)
if got, want := cfg.DockerSandboxes.RecoveryMode, config.DockerSandboxesRecoveryModeExclusiveAuto; got != want {
t.Fatalf("dockerSandboxes.recoveryMode = %q, want wizard value %q", got, want)
}
if got, want := cfg.DockerSandboxes.RecoveryQuiescenceSeconds, config.DockerSandboxesDefaultRecoveryQuiescenceSeconds; got != want {
t.Fatalf("dockerSandboxes.recoveryQuiescenceSeconds = %d, want wizard value %d", got, want)
}
if !strings.Contains(string(configContent), "architectureEmulation: native-only") {
t.Fatalf("wizard config omitted native-only architecture emulation:\n%s", configContent)
}
if !strings.Contains(string(configContent), "recoveryMode: exclusive-auto") || !strings.Contains(string(configContent), "recoveryQuiescenceSeconds: 60") {
t.Fatalf("wizard config omitted exclusive automatic recovery defaults:\n%s", configContent)
}
for key, values := range map[string]struct{ got, want string }{
"rootDisk": {cfg.DockerSandboxes.RootDisk, "auto"},
Expand All @@ -1144,11 +1153,14 @@ func TestInitDockerSandboxesGeneratesDesiredImageConfigAndProvisionsTemplate(t *
t.Fatalf("init output omitted %q:\n%s", want, out.String())
}
}
for _, want := range []string{"Architecture emulation: best-effort", "QEMU/binfmt will be attempted; unsupported hosts continue with verified native containers and a warning."} {
for _, want := range []string{"Architecture emulation: native-only", "QEMU/binfmt is disabled by default; Docker Sandboxes runs native containers only."} {
if !strings.Contains(out.String(), want) {
t.Fatalf("wizard review omitted %q:\n%s", want, out.String())
}
}
if strings.Contains(out.String(), "QEMU/binfmt will be attempted") {
t.Fatalf("wizard review unexpectedly enabled the QEMU attempt by default:\n%s", out.String())
}
setupHints := " Choose how EPAR should provision the reusable runner artifact during startup.\n Docker Sandboxes profiles must include a private Docker daemon; specialized and custom tags are not admitted.\n Image catalog: https://github.com/catthehacker/docker_images#images-available\n\nRunner base image:"
if !strings.Contains(out.String(), setupHints) {
t.Fatalf("Docker Sandboxes setup hints were not grouped before the option list:\n%s", out.String())
Expand Down Expand Up @@ -1288,6 +1300,7 @@ func TestDockerSandboxesPrebuiltConfigRendersExplicitDistributionAndReference(t
"distribution: prebuilt",
"prebuiltReference: " + config.DockerSandboxesPrebuiltActReference,
"sourceImage: ghcr.io/catthehacker/ubuntu:act-latest",
"architectureEmulation: best-effort",
"updateFrequency: weekly",
"updateTime: \"07:00\"",
} {
Expand Down Expand Up @@ -2082,7 +2095,7 @@ func TestInitPromotedDockerSandboxesDefaultsOnlyAfterPassingPreflight(t *testing
if got, want := cfg.DockerSandboxes.PolicyGeneration, record.PolicyFingerprint; got != want {
t.Fatalf("dockerSandboxes.policyGeneration = %q, want %q", got, want)
}
if got, want := cfg.DockerSandboxes.ArchitectureEmulation, config.DockerSandboxesArchitectureEmulationBestEffort; got != want {
if got, want := cfg.DockerSandboxes.ArchitectureEmulation, config.DockerSandboxesArchitectureEmulationNativeOnly; got != want {
t.Fatalf("dockerSandboxes.architectureEmulation = %q, want Windows wizard value %q", got, want)
}
for key, values := range map[string]struct {
Expand All @@ -2100,7 +2113,7 @@ func TestInitPromotedDockerSandboxesDefaultsOnlyAfterPassingPreflight(t *testing
if err != nil {
t.Fatal(err)
}
for _, required := range []string{"type: docker-sandboxes", "dockerSandboxes:", "epar-docker-sandboxes", "policyGeneration: " + record.PolicyFingerprint, "architectureEmulation: best-effort"} {
for _, required := range []string{"type: docker-sandboxes", "dockerSandboxes:", "epar-docker-sandboxes", "policyGeneration: " + record.PolicyFingerprint, "architectureEmulation: native-only", "recoveryMode: exclusive-auto", "recoveryQuiescenceSeconds: 60"} {
if !strings.Contains(string(configText), required) {
t.Fatalf("generated Docker Sandboxes config omitted %q:\n%s", required, configText)
}
Expand All @@ -2113,11 +2126,14 @@ func TestInitPromotedDockerSandboxesDefaultsOnlyAfterPassingPreflight(t *testing
if !strings.Contains(out.String(), "PASS: the exact promoted platform") || !strings.Contains(out.String(), "Docker Sandboxes — recommended (default)") {
t.Fatalf("init output did not explain the promoted default:\n%s", out.String())
}
for _, want := range []string{"Architecture emulation: best-effort", "QEMU/binfmt will be attempted; unsupported hosts continue with verified native containers and a warning."} {
for _, want := range []string{"Architecture emulation: native-only", "QEMU/binfmt is disabled by default; Docker Sandboxes runs native containers only."} {
if !strings.Contains(out.String(), want) {
t.Fatalf("Windows wizard review omitted %q:\n%s", want, out.String())
}
}
if strings.Contains(out.String(), "QEMU/binfmt will be attempted") {
t.Fatalf("Windows wizard review unexpectedly enabled the QEMU attempt by default:\n%s", out.String())
}
}

func TestPromotedDockerSandboxesPlatformUsesSharedHostGuestMapping(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion cmd/ephemeral-action-runner/init_wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ func promptInitReview(out io.Writer, reader *bufio.Reader, draft initWizardDraft
fmt.Fprintf(out, " Architecture emulation: %s\n", architectureEmulation)
if architectureEmulation == config.DockerSandboxesArchitectureEmulationBestEffort {
fmt.Fprintln(out, " QEMU/binfmt will be attempted; unsupported hosts continue with verified native containers and a warning.")
} else if architectureEmulation == config.DockerSandboxesArchitectureEmulationNativeOnly {
fmt.Fprintln(out, " QEMU/binfmt is disabled by default; Docker Sandboxes runs native containers only.")
}
}
default:
Expand Down Expand Up @@ -576,5 +578,5 @@ func renderInitWizardConfig(draft initWizardDraft) (string, error) {
}

func initDockerSandboxesArchitectureEmulation() string {
return config.DockerSandboxesArchitectureEmulationBestEffort
return config.DockerSandboxesArchitectureEmulationNativeOnly
}
9 changes: 6 additions & 3 deletions configs/docker-sandboxes.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ dockerSandboxes:
# The wizard reads and records the exact active host-global policy fingerprint.
policyGeneration: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
networkBaseline: open
# best-effort attempts bundled QEMU/binfmt, then continues with a warning when the sandbox supports native containers only.
# Use required only when every runner must execute foreign-architecture containers.
architectureEmulation: best-effort
# native-only disables QEMU/binfmt by default and verifies the native guest and private Docker architecture.
# Use best-effort or required only after validating the exact foreign-architecture workload.
architectureEmulation: native-only
# exclusive-auto permits one bounded stop-wait-start recovery per create-admission incident; inventory recovery keeps its bounded backoff retries, and observe never mutates sbx.
recoveryMode: exclusive-auto
recoveryQuiescenceSeconds: 60
additionalAllow:
- api.github.com
- '*.githubusercontent.com:443'
Expand Down
4 changes: 2 additions & 2 deletions docs/advanced/cross-architecture-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The setup helper is privileged. Use it only in trusted workflows and treat its p
| Docker Container | Run the setup action inside the disposable job before Docker or Compose uses a foreign image. No EPAR configuration key enables universal emulation. |
| WSL | Run the setup action inside the WSL runner if its Linux Docker daemon needs a foreign image. An x64 WSL runner does not gain ARM64 execution merely by pulling an ARM64 image. |
| Tart on Apple Silicon (retired) | The guest is ARM64. The optional Rosetta path is retained for existing configurations and is not equivalent to QEMU/binfmt. Use a distinct label and validate the exact image/workload. |
| Docker Sandboxes | Keep `provider.platform` native. The default `best-effort` mode tries the bundled QEMU/binfmt handlers and warns when only native execution is available. Set `required` only when foreign execution must be an admission requirement; `native-only` skips the attempt. |
| Docker Sandboxes | Keep `provider.platform` native. The default `native-only` mode skips QEMU/binfmt. Prefer a native or multi-platform image; `best-effort` and `required` are explicit opt-ins and are not a complete cross-architecture guarantee in `sbx` v0.39. |
| GitHub-hosted Windows or macOS | These labels do not replace a Linux Docker daemon for container actions or service containers. Use a suitable Linux execution surface. |

Keep architecture-specific jobs on a distinct `runs-on` label. Do not label an ARM64 runner as `ubuntu-latest`: GitHub's `ubuntu-latest` is a GitHub-managed environment, and x64 assumptions can fail on ARM64.
Expand All @@ -65,7 +65,7 @@ For an amd64-only service on an ARM64 host, first try a published ARM64 or multi

For an ARM64 image on an x64 Linux runner, follow the same process with `platforms: arm64` and `--platform linux/arm64`. Never treat a successful `docker pull` as the proof; run a container and check both the expected architecture output and the real workload.

Docker Sandboxes has no fixed advertised target matrix. `best-effort` and `required` install every handler available from the immutable host-platform `tonistiigi/binfmt` artifact and let QEMU handle an executable when a registered ELF signature matches. In `best-effort`, missing sandbox-kernel support produces a controller warning and foreign images fail normally at execution time while native jobs continue. `native-only` deliberately makes no foreign-execution claim.
Docker Sandboxes has no fixed advertised target matrix. The default `native-only` mode deliberately makes no foreign-execution claim. `best-effort` and `required` install every handler available from the immutable host-platform `tonistiigi/binfmt` artifact and let QEMU handle an executable when a registered ELF signature matches, but `sbx` v0.39 does not fully support this path. In `best-effort`, missing sandbox-kernel support produces a controller warning and foreign images fail normally at execution time while native jobs continue. For a job that requires another architecture, use a native target-architecture EPAR machine with Docker Sandboxes or a separate Docker Container configuration with trusted Docker-in-Docker QEMU support.

## References

Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/docker-sandboxes-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ The direct build does not create a Docker staging image. Once the imported templ

The source lock pins build tooling, Tini, helper inputs, and platform-specific inputs. The default policy checks mutable source and Actions runner selectors weekly at 07:00 local time; `./start image update` checks immediately. EPAR activates a new immutable template only after build, import, and exact readback succeed.

Docker Sandboxes native-platform operation is current on Linux, macOS, and Windows hosts based on completed cross-platform build, import, lifecycle, and cleanup testing. Architecture capability is separate: the wizard uses `best-effort`, which enables QEMU where sandbox-local `binfmt_misc` works and otherwise admits only the verified native platform with a warning. Run local admission and workload validation for the exact host and workload; this status does not claim independent certification. Any future certification record should bind the reviewed native-controller source/build, full template identity, cache ID, metadata/archive digests, architecture mode, and reviewed evidence.
Docker Sandboxes native-platform operation is current on Linux, macOS, and Windows hosts based on completed cross-platform build, import, lifecycle, and cleanup testing. Architecture capability is separate: the wizard uses `native-only`, which skips QEMU and admits only the verified native platform. Run local admission and workload validation for the exact host and workload; this status does not claim independent cross-architecture certification. Any future certification record should bind the reviewed native-controller source/build, full template identity, cache ID, metadata/archive digests, architecture mode, and reviewed evidence.
6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ External-outage supervision is an invocation policy, not YAML. Pass `--external-

| Provider | Host and artifact model | Image defaults | Provider-only configuration |
| --- | --- | --- | --- |
| `docker-sandboxes` | A Linux, macOS, or Windows host with healthy `sbx diagnose --output json` results builds and imports a native Linux runner template. EPAR attempts QEMU/binfmt by default and warns rather than blocking when the sandbox runtime supports native containers only. | `image.distribution: local-build` selects a Catthehacker source for the current build path. The preview `prebuilt` Act path acquires only EPAR's official verified reference and records the exact digest/attestation in local state. | `dockerSandboxes` is required; `provider.platform` is `linux/amd64` or `linux/arm64`; runner-group enforcement must be `enforce`; `architectureEmulation` selects `best-effort`, `required`, or `native-only`. |
| `docker-sandboxes` | A Linux, macOS, or Windows host with healthy `sbx diagnose --output json` results builds and imports a native Linux runner template. EPAR uses native-only architecture admission by default and does not attempt QEMU/binfmt unless the mode is explicitly changed. | `image.distribution: local-build` selects a Catthehacker source for the current build path. The preview `prebuilt` Act path acquires only EPAR's official verified reference and records the exact digest/attestation in local state. | `dockerSandboxes` is required; `provider.platform` is `linux/amd64` or `linux/arm64`; runner-group enforcement must be `enforce`; `architectureEmulation` selects `best-effort`, `required`, or `native-only`. |
| `docker-container` | Compatibility provider: a Docker-compatible host creates an outer disposable runner with its own inner Docker daemon. | `docker-image`, `ghcr.io/catthehacker/ubuntu:full-latest`, output `epar-docker-container-catthehacker-ubuntu`. | Optional `provider.platform`; `docker` proxy and mirror settings apply to its private daemon. |
| `wsl` | Compatibility provider: Windows WSL2 imports a Docker image or rootfs tar into disposable Linux distros. | Docker source defaults to Catthehacker full Ubuntu, x64, with output under `work/images/`. | `provider.installRoot` controls WSL storage. |
| `tart` | Retired Apple Silicon Linux VM path retained for existing configurations and exact runtime/cleanup compatibility; it has no onboarding path. | `ghcr.io/cirruslabs/ubuntu:latest`, output `epar-ubuntu-24-arm64`. | `provider.network` and optional `provider.rosettaTag`. Validate the exact workload before relying on Rosetta. |
Expand Down Expand Up @@ -178,7 +178,9 @@ If the complete subsection is absent, EPAR warns and uses the strict recommended
| `image.sourceImage` | `ghcr.io/catthehacker/ubuntu:full-latest` | Required with Docker Sandboxes; must be the exact `full-latest` or `act-latest` profile. | Desired Catthehacker source selector; EPAR builds and imports the runnable template automatically. Specialized and custom tags remain available to Docker Container and WSL. |
| `policyGeneration` | lowercase `sha256:<64-hex>`; no default | Required with Docker Sandboxes. | Recorded fingerprint of the host-global Balanced policy. |
| `networkBaseline` | `open` or `balanced`; `open` | Docker Sandboxes. | `open` adds a sandbox-scoped public-egress rule while denying host aliases; it does not change the host-global policy. |
| `architectureEmulation` | `best-effort`, `required`, or `native-only`; `best-effort` | Docker Sandboxes. | `best-effort` attempts bundled QEMU handlers, then verifies the native guest and private Docker architecture and continues with a warning when QEMU/binfmt is unavailable. `required` fails creation unless QEMU handlers are active. `native-only` skips the QEMU attempt. |
| `architectureEmulation` | `best-effort`, `required`, or `native-only`; `native-only` | Docker Sandboxes. | `native-only` skips the QEMU attempt and verifies the native guest and private Docker architecture. `best-effort` attempts bundled QEMU handlers and continues with a warning when QEMU/binfmt is unavailable. `required` fails creation unless QEMU handlers are active. |
| `recoveryMode` | `exclusive-auto` or `observe`; `exclusive-auto` | Docker Sandboxes. | `exclusive-auto` is the default and permits EPAR one bounded stop-wait-start recovery attempt per create-admission incident with the known sandbox-container signature; bounded inventory control-plane failures retain their existing probe and backoff retries, but cannot restart the daemon while an admission incident remains open. `observe` leaves those failures unhandled, does not probe or mutate the daemon, and follows existing fail-closed startup or reconciliation behavior; use it only as an explicit maintenance opt-out. |
| `recoveryQuiescenceSeconds` | integer from `1` through `300`; `60` | Docker Sandboxes. | Time that the daemon must remain authoritatively stopped before EPAR starts it again. EPAR also requires repeated stable inventory after the restart. |
| `additionalAllow` | unique hostname or `*.domain`, optional port; empty | Docker Sandboxes. | Adds sandbox-scoped allow resources. With `open`, it cannot re-allow EPAR's host-alias deny guardrails. |
| `additionalDeny` | unique hostname or `*.domain`, optional port; empty | Docker Sandboxes. | Adds sandbox-scoped deny resources. A resource cannot be in both allow and deny lists. |
| `stagingRoot` | canonical project-relative `.local/...` path; `.local/cache/docker-sandboxes/staging` | Docker Sandboxes. | Per-create disposable staging root; cannot be absolute, escape `.local`, or overlap `.local/bin` or `.local/state`. |
Expand Down
Loading
Loading