# Docker Build Service Small HTTP service for CI (e.g. Forgejo Actions) that builds images with **BuildKit** and pushes them to a registry. Callers authorize builds with a JWT signed by a shared secret. The API container does **not** mount the host Docker socket. It talks to BuildKit only over a **Unix socket** on a shared Docker volume (`buildkit-socket`). Nothing else on the host or compose project mounts that volume, so other containers cannot reach BuildKit. Build steps use BuildKit’s default isolated network. Flow: 1. Prepare the build context 2. Verify `sha256(Dockerfile)` matches the JWT claim 3. `buildctl build` (BuildKit) with `push=true` using registry credentials from the request ## Quick start ```bash cp .env.example .env # set JWT_SECRET to a long random value docker compose up --build ``` Health check: `GET http://localhost:8080/health` ## Configuration | Variable | Default | Description | |---|---|---| | `JWT_SECRET` | *(required)* | HS256 secret used to verify build tokens | | `GIT_HOST` | *(required)* | Only host used for clones | | `BUILDKIT_DNS` | *(optional)* | Real upstream DNS IP for `RUN` steps (replaces Docker’s `127.0.0.11`, which BuildKit otherwise turns into `8.8.8.8`) | | `BUILDKIT_HOST` | `unix:///run/buildkit/buildkitd.sock` | BuildKit socket address | | `BUILDKIT_PRUNE_KEEP_DURATION` | `168h` | Keep cache used within this window; older unused cache is pruned by `buildkit-helper` | | `BUILDKIT_PRUNE_INTERVAL_SECONDS` | `86400` | How often `buildkit-helper` runs prune (`86400` = daily) | | `BUILD_SERVICE_PORT` | `8080` | Host port mapped to the API | | `GIT_CLONE_TIMEOUT_SECONDS` | `300` | Max clone duration | | `BUILD_TIMEOUT_SECONDS` | `1800` | Max build/push duration | | `JWT_ALGORITHM` | `HS256` | JWT algorithm | | `MAX_UPLOAD_BYTES` | `209715200` | Max total uploaded file bytes (200 MB) | | `MAX_FILE_BYTES` | `52428800` | Max single uploaded file bytes (50 MB) | | `MAX_MANIFEST_FILES` | `512` | Max paths in JWT `files` claim | ## JWT Tokens must be signed with `JWT_SECRET` and include: | Claim | Description | |---|---| | `repo` | `owner/name` (cloned from `https:////.git` when a git commit is used) | | `dockerfile` | Path to the Dockerfile inside the build context (no `..`) | | `dockerfile_sha256` | Hex SHA-256 of the Dockerfile bytes that will be built | | `exp` | Expiry (standard JWT claim) | For requests that include file uploads, the JWT must also include: | Claim | Description | |---|---| | `files` | Non-empty list of relative file paths. Uploaded paths must match this list exactly. | File parts sent without a JWT `files` claim are ignored. Arbitrary git URLs are rejected. Optional but recommended: keep TTL short (e.g. 15 minutes). Mint tokens from a trusted issuer that has the secret — not from untrusted CI jobs if you can avoid it. ### Mint a token ```bash pip install PyJWT JWT_SECRET='your-secret' python scripts/mint_token.py \ --repo 'org/app' \ --dockerfile Dockerfile \ --hash-from ./Dockerfile \ --ttl-minutes 15 ``` For file uploads, add `--files` with the paths you will send: ```bash JWT_SECRET='your-secret' python scripts/mint_token.py \ --repo 'org/app' \ --dockerfile Dockerfile \ --hash-from ./Dockerfile \ --files Dockerfile src/app.py \ --ttl-minutes 15 ``` `--hash-from` can also be an http(s) URL to the Dockerfile. Or pass the digest directly with `--dockerfile-sha256` instead of `--hash-from`: ```bash JWT_SECRET='your-secret' python scripts/mint_token.py \ --repo 'org/app' \ --dockerfile Dockerfile \ --dockerfile-sha256 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' \ --ttl-minutes 15 ``` With docker: ```bash docker exec -it build-service python scripts/mint_token.py \ --repo 'org/app' \ --dockerfile Dockerfile \ --hash-from https://git.lschaefer.xyz/lukasdotcom/actions-tester/raw/branch/main/Dockerfile \ --ttl-minutes 600000 ``` ## CI action (other repos) Composite action for Forgejo / GitHub Actions — call the build API with `uses:` instead of hand-rolled `curl`. Repo: `https://git.lschaefer.xyz/lukasdotcom/docker-build-container`. ```yaml - name: Trigger buildkit build uses: https://git.lschaefer.xyz/lukasdotcom/docker-build-container@ with: url: http://build-kit.local:8080/build token: ${{ secrets.BUILD }} commit: ${{ needs.build-caddy.outputs.dist_commit }} tags: | lukasdotcom/image:latest lukasdotcom/image:${{ needs.build-caddy.outputs.dist_commit }} username: lukasdotcom password: ${{ secrets.DOCKER }} # optional — private repo clone repo_username: oauth2 repo_password: ${{ secrets.GIT_TOKEN }} ``` `tags` is a newline- or comma-separated list of full image refs. For private repos, pass `repo_username` and `repo_password` together (git credentials for the clone; not the same as registry `username` / `password`). To overlay files (multipart), pass `files` — newline-separated paths used as both the local file and the context path (JWT must include a matching `files` claim): ```yaml files: | Dockerfile dist/app commit: ${{ github.sha }} # optional with uploads ``` The action only needs `bash` and `curl` (no Python). ## API ### `POST /build` #### Git commit Shallow-fetches `commit` from `https:////.git`. ```json { "token": "", "commit": "a1b2c3d4e5f6789012345678901234567890abcd", "tags": [ "lukasdotcom/app:1.0.0", "lukasdotcom/app:latest" ], "username": "registry-user", "password": "registry-password", "repo_username": "oauth2", "repo_password": "" } ``` ```bash curl -N -sS http://localhost:8080/build \ -H 'Content-Type: application/json' \ -d '{ "token": "'"$TOKEN"'", "commit": "'"$COMMIT"'", "tags": ["lukasdotcom/app:latest"], "username": "user", "password": "pass", "repo_username": "oauth2", "repo_password": "'"$GIT_TOKEN"'" }' ``` | Field | Description | |---|---| | `token` | JWT with `repo`, `dockerfile`, and `dockerfile_sha256` | | `commit` | Git commit SHA to build (7–40 hex chars) | | `tags` | One or more image refs to build and push (`registry/name:tag`; safe charset only; max 16) | | `remote` | Optional. Registry host for auth. Defaults to `https://index.docker.io/v1/` (Docker Hub) | | `username` / `password` | Registry credentials | | `repo_username` / `repo_password` | Optional. Git credentials for private clones (both required together) | #### File upload `Content-Type: multipart/form-data`. Clones `commit`, then overlays the uploaded files. The JWT `files` claim must list exactly the uploaded paths. Optional `repo_username` / `repo_password` form fields work the same as in JSON. ```bash curl -N -sS http://localhost:8080/build \ -F "token=$TOKEN" \ -F "commit=$COMMIT" \ -F 'tags=["lukasdotcom/app:latest"]' \ -F "username=$USER" \ -F "password=$PASS" \ -F "files=@./Dockerfile;filename=Dockerfile" \ -F "files=@./src/app.py;filename=src/app.py" ``` Each `files` part uses its multipart `filename` as the relative path. On success, the response is a chunked `text/plain` stream of `buildctl` progress (`--progress=plain`) as the build and push run. Use `curl -N` (or similar) so the client does not buffer the body. JWT / clone / upload / Dockerfile path or hash errors return normal JSON error responses before streaming starts. If the BuildKit build fails after the stream has begun, the HTTP status stays `200` and the stream ends with a line like `[build-service] ERROR: ...`.