- Python 86.4%
- Shell 12.3%
- Dockerfile 1.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| app | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| action.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| requirements.txt | ||
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:
- Prepare the build context
- Verify
sha256(Dockerfile)matches the JWT claim buildctl build(BuildKit) withpush=trueusing registry credentials from the request
Quick start
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_host>/<owner>/<name>.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
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:
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:
JWT_SECRET='your-secret' python scripts/mint_token.py \
--repo 'org/app' \
--dockerfile Dockerfile \
--dockerfile-sha256 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' \
--ttl-minutes 15
With docker:
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.
- name: Trigger buildkit build
uses: https://git.lschaefer.xyz/lukasdotcom/docker-build-container@<sha-or-tag>
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):
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_host>/<owner>/<name>.git.
{
"token": "<jwt>",
"commit": "a1b2c3d4e5f6789012345678901234567890abcd",
"tags": [
"lukasdotcom/app:1.0.0",
"lukasdotcom/app:latest"
],
"username": "registry-user",
"password": "registry-password",
"repo_username": "oauth2",
"repo_password": "<git-token>"
}
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.
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: ....