From f59386246a6b5963c771c99c2e283512f28ec073 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:12:20 +0000 Subject: [PATCH 1/3] Document pushing images to remote registries --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 307543a7..13aaea3d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - **Built-in ingress** — reverse proxy with TLS termination and subdomain routing - **GPU passthrough** — vGPU and VFIO device support - **OCI image support** — pull and run standard container images +- **Remote registry push** — export cached images to any OCI registry (AWS ECR, Docker Hub, ghcr, ...) with docker-style borrowed credentials - **Remote API** — JWT-authenticated server with a separate CLI client ## Requirements @@ -133,6 +134,35 @@ hypeman exec my-app whoami hypeman exec -it my-app /bin/sh ``` +### Pushing Images to Remote Registries + +Images in the local store can be exported to any OCI registry (AWS ECR, +Docker Hub, ghcr, ...) via the API. Credentials follow the docker model: +they stay on the client and are borrowed for a single push, never stored +on the server. Without credentials, the server's own registry logins +(`~/.docker/config.json`) are used. + +```bash +# Push a ready image to a remote registry, lending the client's credentials +curl -X POST https://hypeman.example.com/pushes \ + -H "Authorization: Bearer $HYPEMAN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "image": "myapp:latest", + "target": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1", + "credentials": { + "username": "AWS", + "password": "'$(aws ecr get-login-password --region us-east-1)'" + } + }' + +# Poll the job (queued -> pushing -> pushed | failed) +curl https://hypeman.example.com/pushes/ -H "Authorization: Bearer $HYPEMAN_TOKEN" +``` + +Pushed blobs are identical to the cached image, so manifest digests are +preserved end to end. Only images in the `ready` state can be pushed. + ### VM Lifecycle ```bash From 802a033f2ec7f11fa7edf7d313e818050a7afeb8 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:24:09 +0000 Subject: [PATCH 2/3] Expand remote registry push documentation --- README.md | 93 ++++++++++++++++++++++++++++++-------- config.example.darwin.yaml | 11 +++++ config.example.yaml | 12 ++++- 3 files changed, 96 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 13aaea3d..e38a0e4b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,17 @@ Hypeman is configured via YAML config files. See [`config.example.yaml`](config.example.yaml) (Linux) and [`config.example.darwin.yaml`](config.example.darwin.yaml) (macOS) for all available server options. +To expose the API through Caddy on a public HTTPS hostname, configure the hostname and TLS in the server config. The hostname must also be included in `acme.allowed_domains`: + +```yaml +api: + hostname: api.example.com + tls: true + redirect_http: true +``` + +With this configuration, use `https://api.example.com` as the API base URL. Without it, the API is available on the server's configured port (4973 by default). + ## Usage ```bash @@ -136,32 +147,76 @@ hypeman exec -it my-app /bin/sh ### Pushing Images to Remote Registries -Images in the local store can be exported to any OCI registry (AWS ECR, -Docker Hub, ghcr, ...) via the API. Credentials follow the docker model: -they stay on the client and are borrowed for a single push, never stored -on the server. Without credentials, the server's own registry logins -(`~/.docker/config.json`) are used. +A ready image can be exported asynchronously to any OCI registry through the +remote API. Set the API base URL and API key used by the examples below: + +```bash +export HYPEMAN_BASE_URL="https://api.example.com" +export HYPEMAN_API_KEY="" +``` + +Credentials use the Docker model: + +- If `credentials` is provided, the client lends `username`/`password` or + `registry_token` for this push only. Hypeman uses them in memory and never + persists or logs them. +- If `credentials` is omitted, Hypeman uses the server's Docker keychain, + including `/root/.docker/config.json` or the configured service user's + `~/.docker/config.json` and any credential helpers. +- If Hypeman restarts while a push using borrowed credentials is running, that + job fails instead of retrying without the original credentials. + +Use an HTTPS API URL when sending credentials. The `insecure` request field +controls only the connection from Hypeman to the destination registry. + +For example, push to ECR using a short-lived login password: ```bash -# Push a ready image to a remote registry, lending the client's credentials -curl -X POST https://hypeman.example.com/pushes \ - -H "Authorization: Bearer $HYPEMAN_TOKEN" \ +export ECR_PASSWORD="$(aws ecr get-login-password --region us-east-1)" + +curl --fail-with-body --silent --show-error \ + -X POST "$HYPEMAN_BASE_URL/pushes" \ + -H "Authorization: Bearer $HYPEMAN_API_KEY" \ -H "Content-Type: application/json" \ - -d '{ - "image": "myapp:latest", - "target": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1", - "credentials": { - "username": "AWS", - "password": "'$(aws ecr get-login-password --region us-east-1)'" + --data "{ + \"image\": \"myapp:latest\", + \"target\": \"123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1\", + \"credentials\": { + \"username\": \"AWS\", + \"password\": \"$ECR_PASSWORD\" } - }' + }" +``` -# Poll the job (queued -> pushing -> pushed | failed) -curl https://hypeman.example.com/pushes/ -H "Authorization: Bearer $HYPEMAN_TOKEN" +The response contains a push `id`. Poll it until the status is `pushed` or +`failed`: + +```bash +curl --fail-with-body --silent --show-error \ + "$HYPEMAN_BASE_URL/pushes/" \ + -H "Authorization: Bearer $HYPEMAN_API_KEY" ``` -Pushed blobs are identical to the cached image, so manifest digests are -preserved end to end. Only images in the `ready` state can be pushed. +Push jobs move through `queued`, `pushing`, and `pushed` or `failed`. +`queue_position` is present while a job is queued; successful jobs report +`layers`, `bytes`, and `completed_at`, while failed jobs report `error`. +`GET /pushes` lists jobs newest first. + +Set `"insecure": true` only when the destination registry uses plain HTTP. +HTTPS registries do not need this option. Invalid image names or targets return +`400`, a missing image returns `404`, and only images in the `ready` state can +be pushed (`409 image_not_ready` otherwise). + +Layer blobs are preserved. OCI manifest digests are preserved too, while a +Docker v2 manifest is converted to OCI and can therefore receive a new digest; +use the returned `digest` as the destination manifest digest. + +The default limit is two concurrent pushes. Increase or lower it with: + +```yaml +limits: + max_concurrent_pushes: 2 +``` ### VM Lifecycle diff --git a/config.example.darwin.yaml b/config.example.darwin.yaml index 54787c17..31ab3ef7 100644 --- a/config.example.darwin.yaml +++ b/config.example.darwin.yaml @@ -99,6 +99,16 @@ caddy: internal_dns_port: 5354 stop_on_shutdown: false +# ============================================================================= +# Public API Ingress (optional) +# ============================================================================= +# Exposes the Hypeman API through Caddy. Leave hostname empty to use the API +# directly on port 4973 instead. +# api: +# hostname: api.example.com +# tls: true # requires the hostname in acme.allowed_domains +# redirect_http: true # redirect HTTP requests to HTTPS + # ============================================================================= # Build System Configuration # ============================================================================= @@ -124,6 +134,7 @@ build: limits: max_vcpus_per_instance: 4 max_memory_per_instance: 8GB + # max_concurrent_pushes: 2 # concurrent outbound registry pushes # max_total_volume_storage: "" # 0 or empty = unlimited # ============================================================================= diff --git a/config.example.yaml b/config.example.yaml index ca713c00..ebef4125 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -98,6 +98,16 @@ data_dir: /var/lib/hypeman # internal_dns_port: 0 # 0 = random (for dev); install script sets to 5353 for production # stop_on_shutdown: false # Set to true if you want Caddy to stop when hypeman stops +# ============================================================================= +# Public API Ingress (optional) +# ============================================================================= +# Exposes the Hypeman API through Caddy. Leave hostname empty to use the API +# directly on port 4973 instead. +# api: +# hostname: api.example.com +# tls: true # requires the hostname in acme.allowed_domains +# redirect_http: true # redirect HTTP requests to HTTPS + # ============================================================================= # TLS / ACME Configuration (for HTTPS ingresses) # ============================================================================= @@ -168,6 +178,6 @@ data_dir: /var/lib/hypeman # max_memory_per_instance: 32GB # max_total_volume_storage: "" # 0 or empty = unlimited # max_concurrent_builds: 1 -# max_concurrent_pushes: 2 +# max_concurrent_pushes: 2 # concurrent outbound registry pushes # push_timeout: 30m # max_overlay_size: 100GB From a6d7a09a9400f547272bb809dd88d24e72804370 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:24:48 +0000 Subject: [PATCH 3/3] Document push recovery behavior --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e38a0e4b..ef1f0fe2 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,8 @@ Credentials use the Docker model: including `/root/.docker/config.json` or the configured service user's `~/.docker/config.json` and any credential helpers. - If Hypeman restarts while a push using borrowed credentials is running, that - job fails instead of retrying without the original credentials. + job fails instead of retrying without the original credentials. Anonymous + interrupted jobs can be recovered with the server's keychain. Use an HTTPS API URL when sending credentials. The `insecure` request field controls only the connection from Hypeman to the destination registry.