Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bb339cc
Adds CLAUDE.md
jphenow Feb 6, 2026
8fc7938
chore: upgrade Docker base image and build dependencies
jphenow Feb 6, 2026
fb78692
build: update accelerated-container-image and overlaybd snapshotter v…
jphenow Feb 6, 2026
183f1fe
chore: update Go dependencies to support Docker 25.0.5
jphenow Feb 6, 2026
420fbfe
build: upgrade Go and dependency versions for Docker 25 compatibility
jphenow Feb 6, 2026
c1bd8a9
test: add comprehensive Docker 25 upgrade validation suite
jphenow Feb 6, 2026
86bb785
docs: record Docker 25 upgrade test results and validation
jphenow Feb 6, 2026
cb885e0
chore: expand .dockerignore for cleaner Docker images
jphenow Feb 7, 2026
5149354
chore: add pre-commit hooks and lint targets
jphenow Feb 7, 2026
42566f1
ci: restructure workflow with separate lint, test, and build jobs
jphenow Feb 7, 2026
1ac68ae
test: implement comprehensive tiered test suite
jphenow Feb 7, 2026
29db88d
remove interim artifact
jphenow Feb 7, 2026
f9a2508
test: use dynamic repository root detection instead of hardcoded paths
jphenow Feb 8, 2026
f9842eb
ci: restrict push trigger to main branch only
jphenow Feb 8, 2026
06391c1
test: use --entrypoint flag to skip docker-entrypoint.d scripts
jphenow Feb 8, 2026
976624c
chore: upgrade Go from 1.21 to 1.24
jphenow Feb 9, 2026
83897e3
chore: update build artifacts and reference links
jphenow Feb 9, 2026
0611c51
test: remove legacy monolithic test scripts
jphenow Feb 9, 2026
c3472a2
test: add Buildx and Alpine version checks to version verification
jphenow Feb 9, 2026
119f0b6
build: remove explicit docker-buildx copy from Dockerfile
jphenow Feb 9, 2026
9c102b5
test: remove tier1 version check test
jphenow Feb 9, 2026
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
35 changes: 34 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
# Fly.io specific
fly.toml
id_rsa
id_rsa

# Git
.git
.gitignore
.github

# Tests (not needed in image)
tests/
TEST_RESULTS.md

# Documentation
*.md
!README.md

# Development files
.pre-commit-config.yaml
Vagrantfile
.vagrant

# Build artifacts
*.log
*.tmp

# IDE
.vscode
.idea
*.swp
*.swo

# OS
.DS_Store
Thumbs.db
176 changes: 150 additions & 26 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,178 @@ name: ci
on:
push:
branches:
- '**'
- 'main'
tags:
- 'v*.*.*'
pull_request:
branches:
- 'main'

workflow_dispatch:
inputs:
test_tier:
description: 'Test tier to run'
required: false
default: 'tier1'
type: choice
options:
- tier1
- tier2
- tier3

# Cancel in-progress runs for same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
main:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'dockerproxy/go.mod'
cache-dependency-path: 'dockerproxy/go.sum'

- name: Run go vet
working-directory: ./dockerproxy
run: go vet ./...

- name: Check formatting
working-directory: ./dockerproxy
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Code is not properly formatted:"
gofmt -l .
exit 1
fi

- name: Verify go modules
working-directory: ./dockerproxy
run: go mod verify

test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'dockerproxy/go.mod'
cache-dependency-path: 'dockerproxy/go.sum'

- name: Run Go unit tests
working-directory: ./dockerproxy
run: go test -v ./...

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image for testing
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
tags: flyio/rchab:test
build-args: |
BUILD_SHA=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Determine test tier
id: tier
run: |
# Default tiers based on event
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TIER="${{ github.event.inputs.test_tier }}"
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
TIER="tier3"
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
TIER="tier2"
else
TIER="tier1"
fi

# Check for PR labels to force higher tiers
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'test:tier3') }}" == "true" ]]; then
TIER="tier3"
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'test:tier2') }}" == "true" ]]; then
TIER="tier2"
fi
fi

echo "tier=${TIER}" >> $GITHUB_OUTPUT
echo "Running test tier: ${TIER}"

- name: Run Tier 1 tests (fast)
if: steps.tier.outputs.tier == 'tier1' || steps.tier.outputs.tier == 'tier2' || steps.tier.outputs.tier == 'tier3'
run: |
cd tests
./run-tests.sh tier1

- name: Run Tier 2 tests (critical)
if: steps.tier.outputs.tier == 'tier2' || steps.tier.outputs.tier == 'tier3'
run: |
cd tests
./run-tests.sh tier2

- name: Run Tier 3 tests (full)
if: steps.tier.outputs.tier == 'tier3'
run: |
cd tests
./run-tests.sh tier3

build:
name: Build and Push
needs: [lint, test]
runs-on: ubuntu-latest
steps:
-
name: Docker meta
- name: Checkout code
uses: actions/checkout@v4

- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
flyio/rchab
# generate Docker tags based on the following events/attributes
images: flyio/rchab
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.FLYIOBUILDS_DOCKERHUB_USERNAME }}
password: ${{ secrets.FLYIOBUILDS_DOCKERHUB_TOKEN }}
-
name: Build and push

- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_SHA=${{ github.sha }}
-
name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
id_rsa
.vagrant/

dockerproxy/dockerproxy
24 changes: 24 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: local
hooks:
- id: go-mod-tidy
name: go mod tidy
entry: bash -c 'cd dockerproxy && go mod tidy'
language: system
files: \.go$
pass_filenames: false

- id: go-fmt
name: go fmt
entry: gofmt
args: [-w]
language: system
files: \.go$
90 changes: 90 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What is rchab?

**Remote Controlled Hot Air Balloon** — a Docker proxy that provides remote Docker build infrastructure for Fly.io. When users run `flyctl deploy --remote-only`, flyctl provisions an rchab instance as the organization's remote builder. It runs inside a Fly Machine, managing its own Docker daemon and exposing a proxied Docker API with auth and storage management.

## Architecture

The application has two main layers:

1. **Container (root level)** — Dockerfile builds a multi-stage image based on `docker:25.0.5-alpine3.20`. It bundles dockerd, buildx, overlaybd (accelerated container images), and the Go proxy binary. The entrypoint runs `docker-entrypoint.d/` scripts (Docker data dir setup, sysctl tuning) then starts the proxy.

2. **`dockerproxy/` (Go application)** — An HTTP reverse proxy that sits in front of dockerd. All source files are in a single flat package (`package main`):
- `main.go` — HTTP server setup, reverse proxy to dockerd (`localhost:2376`), auto-shutdown idle timer, path filtering, middleware chain (logging → HTTPS upgrade → auth → handler)
- `auth.go` — Bearer token auth against the Fly API; validates the requesting app belongs to the same org as the builder. Results are cached with `go-cache`.
- `dockerd.go` — Launches and manages the dockerd process, health-checks it, watches for active builds (Docker containers + buildkit/runc processes) to keep the machine alive.
- `storage.go` — Disk space monitoring and Docker pruning (images, volumes, build cache) when usage exceeds thresholds.
- `overlaybd.go` — Handler for converting Docker images to overlaybd format.
- `error.go` / `error_test.go` — Insufficient storage error helper.

**Key HTTP endpoints:**
- `/` — Reverse proxy to dockerd (filtered by `allowedPaths` regex list, though filtering is currently disabled via `noFilter = true`)
- `/flyio/v1/extendDeadline` — Extends the auto-shutdown timer, prunes if storage is low
- `/flyio/v1/prune` — Triggers Docker resource pruning
- `/flyio/v1/buildOverlaybdImage` — Converts images to overlaybd format
- `/flyio/v1/settings` — Returns builder capabilities

**Two HTTP servers run simultaneously:**
- `:8080` — Public-facing with auth + HTTPS middleware (fronted by Fly's proxy)
- `:2375` — Raw Docker proxy (no auth, used over 6PN internal network)

**Auto-shutdown:** The machine shuts itself down after 10 minutes of inactivity. Active Docker containers and buildkit (runc) processes reset the timer.

## Build & Development Commands

### Local development (requires Vagrant — manages its own Docker daemon)
```shell
vagrant up # provision VM with Docker + Go
vagrant ssh
cd rchab
make run-local-no-auth # run without auth (for local testing)
```

### Build Docker image
```shell
make build-docker # build locally (linux/amd64)
make build-and-push-docker # build and push to flyio/rchab registry
```

### Run Go tests
```shell
cd dockerproxy && go test ./...
```

### Test with flyctl locally
```shell
FLY_REMOTE_BUILDER_HOST_WG=1 FLY_RCHAB_OVERRIDE_HOST=tcp://127.0.0.1:2375 LOG_LEVEL=debug fly deploy --remote-only
```

### Test with an organization
```shell
fly orgs builder update <your_org> <image_ref>
```

## Environment Variables

| Variable | Purpose |
|---|---|
| `NO_DOCKERD` | Skip launching dockerd (use existing) |
| `NO_AUTH` | Disable auth middleware |
| `NO_APP_NAME` | Skip org membership check |
| `NO_HTTPS` | Disable HTTPS redirect |
| `LOG_LEVEL` | Logrus log level (default: `info`) |
| `FLY_APP_NAME` | Builder's own app name (set by Fly runtime) |
| `DATA_DIR` | Data directory path |

## CI/CD

GitHub Actions (`.github/workflows/ci.yaml`) builds and pushes the Docker image to DockerHub (`flyio/rchab`) on every branch push and tag. PRs only build without pushing. Fly.io staff must make an internal update for the new image to become the default builder.

## Key Dependencies

- Go 1.24, Docker 25.0.5
- `github.com/superfly/flyctl/api` — Fly API client for auth
- `github.com/gorilla/handlers` — HTTP logging middleware
- `github.com/minio/minio/pkg/disk` — Disk usage info
- `github.com/patrickmn/go-cache` — In-memory auth cache
- `github.com/mitchellh/go-ps` — Process listing (buildkit detection)
Loading