Docs navigation

Platform

Registry

A private container registry for your own images, one namespace per organization.

Lunary runs a container registry at registry.lunary.cloud. Your images live under your organization slug: registry.lunary.cloud/<org>/<name>. That prefix is your namespace and the only place your tokens can push. Private by default: a repository is readable by your own tokens and your own VMs, and by nothing else. There is no public option yet.

GET /api/v1/registry (read_instances) returns the host, your namespace, the prefix to tag with, and what docker login needs. Where the registry is not offered it returns enabled: false with the other fields null.

Pushing

Log in with an API token carrying the push_images scope as the password. The username is not checked; use lunary.

echo "$LUNARY_TOKEN" | docker login registry.lunary.cloud -u lunary --password-stdin
docker build -t registry.lunary.cloud/<org>/<name>:1.4.2 .
docker push registry.lunary.cloud/<org>/<name>:1.4.2

Tag every push. The deploy spec requires a tag or digest on every image reference; a bare name is refused. A tag is re-resolved on every redeploy and a digest is not, so pin a digest when a deploy must mean exactly one build.

A token without push_images or pull_images logs in but can neither push nor pull. Tokens are minted under Access → API tokens; see Authentication.

Pushing stops at your plan's image cap: once your namespace holds that much, the registry answers a push with denied: image storage over quota until a tag is deleted or the plan grows. Nothing is deleted for you. The cap is checked against the last reading, which is taken a few minutes after a push, so the push that crosses it lands and the next one is refused; pushes within the same few minutes are judged on the reading before them.

Pulling from your own servers

A box that is not a Lunary VM pulls with a token carrying only pull_images: a docker login that can read your namespace and nothing else, not push, not the API. Mint one under Access → API tokens as Pull images only, then:

echo "$LUNARY_PULL_TOKEN" | docker login registry.lunary.cloud -u lunary --password-stdin
docker pull registry.lunary.cloud/<org>/<name>:1.4.2

Deploying from GitHub Actions

One job builds the image, pushes it under two tags, and moves the instance to the commit's tag. LUNARY_TOKEN is a repository secret holding a token with push_images and write_instances.

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: docker/login-action@v4
        with:
          registry: registry.lunary.cloud
          username: lunary
          password: ${{ secrets.LUNARY_TOKEN }}

      - uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: |
            registry.lunary.cloud/<org>/<name>:sha-${{ github.sha }}
            registry.lunary.cloud/<org>/<name>:latest

      - name: Deploy the new image and wait for the run
        run: |
          set -o pipefail
          run_id=$(curl -fsS --max-time 30 -X PATCH https://lunary.cloud/api/v1/instances/<slug> \
            -H "Authorization: Bearer ${{ secrets.LUNARY_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{"image": "registry.lunary.cloud/<org>/<name>:sha-${{ github.sha }}"}' \
            | jq -r .meta.run_id)
          [ -n "$run_id" ] && [ "$run_id" != null ] || { echo "PATCH returned no run id"; exit 1; }
          deadline=$(( $(date +%s) + 600 ))
          while [ "$(date +%s)" -lt "$deadline" ]; do
            status=$(curl -fsS --max-time 30 https://lunary.cloud/api/v1/runs/$run_id \
              -H "Authorization: Bearer ${{ secrets.LUNARY_TOKEN }}" | jq -r .data.status) || status=unknown
            case $status in
              succeeded) exit 0 ;;
              failed|halted|unauthorized) echo "run $run_id $status"; exit 1 ;;
            esac
            sleep 10
          done
          echo "run $run_id still $status after 10 minutes"; exit 1

The PATCH answers 202 as soon as the redeploy is queued, so the job has to wait for the run (GET /api/v1/runs/{run_id}) before its result means anything. :latest is there for docker pull; deploy the sha- tag so every deploy names one commit.

Deploying

An image on the registry deploys like any other. Provision with POST /api/v1/instances, catalog_slug: "custom" and the image in custom.image, or move a running instance with PATCH /api/v1/instances/{slug}. See Custom apps.

No registry credential is stored or needed. Your VM pulls with a short-lived grant the control plane mints for each deploy: bound to your namespace, pull only, good for minutes. POST /api/v1/registries is for other registries.

Listing

In the dashboard, Access → Registries shows the host, your namespace, the login recipe and every image pushed so far.

GET /api/v1/registry/images (read_instances) returns every repository in your namespace with its tags, sorted, and the digest each tag points at:

{
  "data": [
    {
      "repository": "acme/app",
      "name": "app",
      "tags": [
        { "name": "latest", "digest": "sha256:9f86d0…" },
        { "name": "sha-1a2b3c4", "digest": "sha256:9f86d0…" }
      ]
    }
  ]
}

The list is read from the registry as you call, so it is bounded and never paginated. 503 registry_disabled means the registry is not offered; 503 registry_unavailable means it did not answer. Over MCP the same data is list_images, and get_registry mirrors GET /api/v1/registry.

Deleting

A tag is the unit of retention. DELETE /api/v1/registry/images/{name}/tags/{tag} (push_images) removes one; 204 when it is gone. The dashboard has a Delete beside every tag, and MCP has delete_image_tag. Every delete is in your audit log.

The image itself stays until the nightly sweep finds no tag pointing at it. So deleting sha-1a2b3c4 while latest points at the same build frees nothing, and deleting the last tag of a build frees it that night. A deploy pinned to a digest survives only while some tag still points at that digest: keep a tag on anything you pin.

409 image_in_use refuses the delete while an instance deploys from that tag, or from the digest it points at, and names the instances. Move them to another image first.

A CI job that tags every commit can prune its own history after the deploy: list the repository, keep the tags of the last few commits, delete the rest. The step below keeps ten; a 409 is a tag still deployed and stays.

keep=$(git rev-list --first-parent -n 10 HEAD)
curl -fsS https://lunary.cloud/api/v1/registry/images -H "Authorization: Bearer $LUNARY_TOKEN" \
  | jq -r '.data[] | select(.name == "<name>") | .tags[].name | select(startswith("sha-"))' \
  | while read -r tag; do
      grep -qx "${tag#sha-}" <<<"$keep" && continue
      curl -sS -o /dev/null -w "$tag %{http_code}\n" -X DELETE \
        "https://lunary.cloud/api/v1/registry/images/<name>/tags/$tag" \
        -H "Authorization: Bearer $LUNARY_TOKEN"
    done

Retention and quota

Your plan caps the storage your images reference, counted once per layer across your namespace (image_storage_gib; the dashboard's Access → Registries shows the reading and the cap, and the admin can raise it per organization). The reading is taken a few minutes after each push and every quarter hour. Once a reading is at the cap, pushes are refused as above until a tag is deleted; pulls, deploys and the images already there are untouched. It is a cap you can be over, not one you cannot exceed. Nothing is pruned on your behalf: you choose which tags to delete.

Untagged images, pushed by digest alone or left behind when every tag moved on, are removed by the nightly sweep and are not counted. Tag what you want kept.

Durability

The registry keeps one copy of your images, in Canadian object storage. An image is a build artifact: if the registry lost it, your CI pushes it again from source, and the only thing lost in between is the ability to roll back to it. Keep your builds reproducible, or keep a copy of a release you cannot rebuild.

Reading with an agent? This page as markdown, or the whole docs as llms.txt.