From cc255278306510691eb5b77669c8846ed650eda8 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Wed, 11 Mar 2026 19:55:02 +1000 Subject: [PATCH 1/3] Fix Docker socket permission denied in panda init server container The published server image (goreleaser.server.Dockerfile) runs as USER panda (UID 1000) with no Docker CLI and no docker group, so mounting the Docker socket always fails with permission denied. - Install docker-cli in the server image - Detect the host Docker socket GID during panda init and inject it via group_add in the generated compose file - Add CI smoke test that builds the server image and verifies Docker socket access from inside the container --- .github/workflows/docker.yml | 25 +++++++++++++++++++++++++ goreleaser.server.Dockerfile | 2 +- pkg/cli/init.go | 28 +++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 548fa03..be5b5bb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -58,3 +58,28 @@ jobs: - name: Build sandbox image run: docker build -f sandbox/Dockerfile . + + server-docker-smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build server binary + run: | + mkdir -p linux/amd64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o linux/amd64/panda-server ./cmd/server + + - name: Build server image + run: docker build -f goreleaser.server.Dockerfile -t panda-server-smoke . + + - name: Verify Docker socket access from server container + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --group-add "$(stat -c '%g' /var/run/docker.sock)" \ + panda-server-smoke \ + docker info --format '{{.ServerVersion}}' diff --git a/goreleaser.server.Dockerfile b/goreleaser.server.Dockerfile index 8846773..df69184 100644 --- a/goreleaser.server.Dockerfile +++ b/goreleaser.server.Dockerfile @@ -22,7 +22,7 @@ RUN mkdir -p /model/all-MiniLM-L6-v2 && \ # ============================================================================= FROM alpine:3.21 -RUN apk add --no-cache ca-certificates tzdata +RUN apk add --no-cache ca-certificates tzdata docker-cli RUN addgroup -g 1000 panda && \ adduser -u 1000 -G panda -D panda diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 2d89f84..4317a19 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -7,6 +7,8 @@ import ( "os" "os/exec" "path/filepath" + "strconv" + "syscall" "time" dockerimage "github.com/docker/docker/api/types/image" @@ -197,6 +199,8 @@ proxy: } func buildComposeTemplate(serverImage, configDir string) string { + dockerGID := detectDockerSocketGID() + return fmt.Sprintf(`# panda server - Docker Compose configuration # Generated by 'panda init'. Managed by 'panda server' commands. @@ -205,6 +209,8 @@ services: image: %s container_name: panda-server restart: unless-stopped + group_add: + - "%s" ports: - "127.0.0.1:2480:2480" volumes: @@ -223,7 +229,7 @@ networks: volumes: panda-storage: -`, serverImage, configDir) +`, serverImage, dockerGID, configDir) } func checkDockerAndPullImages() error { @@ -298,3 +304,23 @@ func pullImage(cli *dockerclient.Client, image string) error { return nil } + +// detectDockerSocketGID returns the group ID that owns /var/run/docker.sock. +// This is used to add the correct group to the server container so the +// non-root panda user can access the Docker socket. Falls back to "0" (root) +// if the socket cannot be stat'd. +func detectDockerSocketGID() string { + const dockerSocket = "/var/run/docker.sock" + + info, err := os.Stat(dockerSocket) + if err != nil { + return "0" + } + + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return "0" + } + + return strconv.FormatUint(uint64(stat.Gid), 10) +} From 818061ed1f1d233b692601bea2916c7585d414e1 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Wed, 11 Mar 2026 19:56:36 +1000 Subject: [PATCH 2/3] Fold smoke test into goreleaser-check job to avoid redundant build --- .github/workflows/docker.yml | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index be5b5bb..b7e00e8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -51,35 +51,21 @@ jobs: env: LATEST_TAG: latest - build-sandbox: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build sandbox image - run: docker build -f sandbox/Dockerfile . - - server-docker-smoke-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Build server binary + - name: Smoke test server Docker socket access run: | mkdir -p linux/amd64 - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o linux/amd64/panda-server ./cmd/server - - - name: Build server image - run: docker build -f goreleaser.server.Dockerfile -t panda-server-smoke . - - - name: Verify Docker socket access from server container - run: | + cp dist/panda-server_linux_amd64_v1/panda-server linux/amd64/panda-server + docker build -f goreleaser.server.Dockerfile -t panda-server-smoke . docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ --group-add "$(stat -c '%g' /var/run/docker.sock)" \ panda-server-smoke \ docker info --format '{{.ServerVersion}}' + + build-sandbox: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sandbox image + run: docker build -f sandbox/Dockerfile . From 53d507ae827cbd5699713abe972ae9c89331d6ab Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Wed, 11 Mar 2026 20:00:08 +1000 Subject: [PATCH 3/3] Migrate all CI workflows to self-hosted runners and add proper smoke test - Replace ubuntu-latest with self-hosted-ghr runners across all workflows - Use size-l-x64 for goreleaser/release builds, size-m-x64 for everything else - Upgrade smoke test to verify actual server sandbox initialization (Docker access, image pull, network creation) instead of just docker info --- .github/workflows/build-master.yaml | 4 ++- .github/workflows/build-pr.yaml | 8 +++-- .github/workflows/ci.yml | 8 +++-- .github/workflows/docker.yml | 48 ++++++++++++++++++++++++++--- .github/workflows/eval.yaml | 8 +++-- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-master.yaml b/.github/workflows/build-master.yaml index 133350e..dd0f61e 100644 --- a/.github/workflows/build-master.yaml +++ b/.github/workflows/build-master.yaml @@ -6,7 +6,9 @@ on: jobs: meta: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 outputs: short-sha: ${{ steps.sha.outputs.short }} steps: diff --git a/.github/workflows/build-pr.yaml b/.github/workflows/build-pr.yaml index 6dcc7bf..5153525 100644 --- a/.github/workflows/build-pr.yaml +++ b/.github/workflows/build-pr.yaml @@ -7,7 +7,9 @@ on: jobs: meta: if: contains(github.event.pull_request.labels.*.name, 'build') && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 outputs: short-sha: ${{ steps.sha.outputs.short }} steps: @@ -31,7 +33,9 @@ jobs: comment: needs: [meta, build] if: contains(github.event.pull_request.labels.*.name, 'build') && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 steps: - name: Comment PR with image tags uses: actions/github-script@v7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5648e37..768c06c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,9 @@ concurrency: jobs: test: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 steps: - uses: actions/checkout@v4 @@ -45,7 +47,9 @@ jobs: run: make test lint: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b7e00e8..5fde2ca 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -29,7 +29,9 @@ concurrency: jobs: goreleaser-check: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-l-x64 steps: - uses: actions/checkout@v4 with: @@ -51,19 +53,55 @@ jobs: env: LATEST_TAG: latest - - name: Smoke test server Docker socket access + - name: Build server and sandbox images run: | mkdir -p linux/amd64 cp dist/panda-server_linux_amd64_v1/panda-server linux/amd64/panda-server docker build -f goreleaser.server.Dockerfile -t panda-server-smoke . - docker run --rm \ + docker build -f sandbox/Dockerfile -t panda-sandbox-smoke . + + - name: Smoke test server sandbox initialization + run: | + # Write a minimal config that the server can parse. + # The server will initialize the sandbox (Docker access, image pull, + # network creation) before attempting to connect to the proxy. + cat > /tmp/smoke-config.yaml <<'CONFIG' + server: + host: "0.0.0.0" + port: 2480 + transport: sse + sandbox: + image: "panda-sandbox-smoke:latest" + network: "panda-smoke-test" + proxy: + url: "http://localhost:19999" + CONFIG + + # Run the server and capture logs. It will fail at proxy connection, + # but we check that sandbox initialization completed successfully. + LOGS=$(docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp/smoke-config.yaml:/app/config.yaml:ro \ --group-add "$(stat -c '%g' /var/run/docker.sock)" \ panda-server-smoke \ - docker info --format '{{.ServerVersion}}' + serve --config /app/config.yaml 2>&1 || true) + + echo "$LOGS" + + if echo "$LOGS" | grep -q "Sandbox service started"; then + echo "Smoke test passed: sandbox initialized successfully" + else + echo "Smoke test failed: sandbox did not initialize" + exit 1 + fi + + # Clean up the network created by the server. + docker network rm panda-smoke-test 2>/dev/null || true build-sandbox: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/eval.yaml b/.github/workflows/eval.yaml index 9e58de3..d5918c6 100644 --- a/.github/workflows/eval.yaml +++ b/.github/workflows/eval.yaml @@ -40,7 +40,9 @@ on: jobs: evaluate: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 timeout-minutes: 60 steps: @@ -224,7 +226,9 @@ jobs: comment_mode: always compare-models: - runs-on: ubuntu-latest + runs-on: + - self-hosted-ghr + - size-m-x64 needs: evaluate if: github.event_name == 'workflow_dispatch' && inputs.model == 'claude-sonnet-4-5'