| 1 | # Container registry |
| 2 | |
| 3 | Diffinery hosts container images at `diffinery.com`, next to the code that builds |
| 4 | them. It speaks the standard OCI distribution API, so `docker`, `podman`, |
| 5 | `buildx`, `crane`, `oras`, `skopeo` and the `docker/*` GitHub Actions work |
| 6 | without changes. Permissions follow ghcr.io: images belong to an owner, are |
| 7 | private by default, and can be linked to a repository whose collaborators then |
| 8 | share access to them. |
| 9 | |
| 10 | ## Image names |
| 11 | |
| 12 | ``` |
| 13 | diffinery.com/<owner>/<name>[:<tag>|@sha256:<digest>] |
| 14 | ``` |
| 15 | |
| 16 | `<owner>` is a user or organization. `<name>` may contain slashes |
| 17 | (`diffinery.com/acme/api/worker`). Names are lowercase; `docker` requires that |
| 18 | anyway. Blobs and manifests are stored in R2, and identical layers are stored |
| 19 | once across all packages. |
| 20 | |
| 21 | ## Signing in |
| 22 | |
| 23 | Use any username and a Diffinery token as the password. Create a token under |
| 24 | **Settings → Tokens**: the `read` scope pulls, `write` pushes, and `admin` also |
| 25 | deletes. A token limited to some repositories only reaches packages linked to |
| 26 | those repositories. |
| 27 | |
| 28 | ```sh |
| 29 | echo "$DIFFINERY_TOKEN" | docker login diffinery.com -u me --password-stdin |
| 30 | docker push diffinery.com/acme/api:1.4.0 |
| 31 | docker pull diffinery.com/acme/api:1.4.0 |
| 32 | ``` |
| 33 | |
| 34 | ## In workflows |
| 35 | |
| 36 | The job's `GITHUB_TOKEN` signs in, exactly as with ghcr.io. Its `packages` |
| 37 | permission decides what it can do: |
| 38 | |
| 39 | | `permissions` | `GITHUB_TOKEN` can | |
| 40 | |---|---| |
| 41 | | none set, or `write-all` | push and pull | |
| 42 | | `packages: write` | push and pull | |
| 43 | | `packages: read`, or `read-all` | pull | |
| 44 | | a map without `packages` | nothing | |
| 45 | |
| 46 | With write, a workflow pushes to packages linked to its own repository, |
| 47 | creates new packages under its repository's owner (linked to the repository), |
| 48 | and deletes versions of its repository's packages. Other packages are |
| 49 | read-only to it, including ones not linked to any repository. With read, it pulls its |
| 50 | own repository's packages and any other package of the same owner that it can |
| 51 | see. |
| 52 | |
| 53 | ```yaml |
| 54 | jobs: |
| 55 | image: |
| 56 | runs-on: ubuntu-latest |
| 57 | permissions: |
| 58 | contents: read |
| 59 | packages: write |
| 60 | steps: |
| 61 | - uses: actions/checkout@v4 |
| 62 | - uses: docker/setup-buildx-action@v3 |
| 63 | - uses: docker/login-action@v3 |
| 64 | with: |
| 65 | registry: diffinery.com |
| 66 | username: ${{ github.actor }} |
| 67 | password: ${{ secrets.GITHUB_TOKEN }} |
| 68 | - uses: docker/build-push-action@v6 |
| 69 | with: |
| 70 | push: true |
| 71 | tags: diffinery.com/${{ github.repository }}:${{ github.sha }} |
| 72 | ``` |
| 73 | |
| 74 | `github.repository` keeps the owner's capitals; use a lowercase name if the |
| 75 | owner or repository has any (`${GITHUB_REPOSITORY,,}` in a shell step). |
| 76 | |
| 77 | Plain `docker login` / `docker build` / `docker push` in `run:` steps works the |
| 78 | same way. A later job pulls with `docker pull`, as a job `container:`, or as a |
| 79 | `services:` image, after signing in with `credentials:`: |
| 80 | |
| 81 | ```yaml |
| 82 | container: |
| 83 | image: diffinery.com/acme/api:${{ github.sha }} |
| 84 | credentials: |
| 85 | username: ${{ github.actor }} |
| 86 | password: ${{ secrets.GITHUB_TOKEN }} |
| 87 | ``` |
| 88 | |
| 89 | ## Moving from ghcr.io |
| 90 | |
| 91 | Replace `ghcr.io` with `diffinery.com` in image names and in `registry:` for |
| 92 | `docker/login-action`. Nothing else changes: the same `permissions: packages: |
| 93 | write`, the same `GITHUB_TOKEN`, the same `org.opencontainers.image.source` |
| 94 | label. To copy existing images across, use a tool that copies all platforms |
| 95 | (this works for layers under 100 MB; for bigger ones, `docker pull`, `docker |
| 96 | tag` and `docker push` in a hosted workflow, see [Size limits](#size-limits)): |
| 97 | |
| 98 | ```sh |
| 99 | crane auth login diffinery.com -u me -p "$DIFFINERY_TOKEN" |
| 100 | crane copy ghcr.io/acme/api:1.4.0 diffinery.com/acme/api:1.4.0 |
| 101 | ``` |
| 102 | |
| 103 | ## Repositories and visibility |
| 104 | |
| 105 | A package linked to a repository shares the repository's roles: readers pull, |
| 106 | writers push, admins delete versions and change settings. The owner (a user, |
| 107 | or an organization's owners and admins) always administers its packages. |
| 108 | Organization members can create packages and pull the organization's packages; |
| 109 | a package that isn't linked to a repository is administered by the member who |
| 110 | created it, and other members can't push to it. |
| 111 | |
| 112 | A package is linked when it is created: |
| 113 | |
| 114 | - by a workflow, to the workflow's repository; |
| 115 | - by a person pushing `<owner>/<repo>` or `<owner>/<repo>/<anything>` while |
| 116 | able to write to that repository; |
| 117 | - by the `org.opencontainers.image.source` label or annotation on the image, |
| 118 | when it names a repository of the same owner (on Diffinery or on github.com) |
| 119 | that the pusher can write to. An unlinked package is linked the first time |
| 120 | an image with the label is pushed. |
| 121 | |
| 122 | The link can also be changed on the package's settings page or through the |
| 123 | API. Packages are **private** by default; **public** lets anyone pull without |
| 124 | signing in. |
| 125 | |
| 126 | Pages: |
| 127 | |
| 128 | - `https://diffinery.com/<owner>/-/packages`: an owner's packages. |
| 129 | - `https://diffinery.com/<owner>/-/packages/container/<name>`: tags, versions, |
| 130 | pull commands and settings. |
| 131 | - `https://diffinery.com/<owner>/<repo>/packages`: the packages linked to a |
| 132 | repository (the **Packages** tab). |
| 133 | |
| 134 | ## API |
| 135 | |
| 136 | The REST API follows GitHub's packages API under `/api/v3`, so existing |
| 137 | scripts work after changing the base URL. `<type>` is always `container`; |
| 138 | `<name>` is URL-encoded when it contains slashes (`api%2Fworker`). |
| 139 | |
| 140 | | Request | Does | |
| 141 | |---|---| |
| 142 | | `GET /user/packages?package_type=container` | your packages | |
| 143 | | `GET /users/<user>/packages`, `GET /orgs/<org>/packages` | an owner's packages you can see (`?visibility=` filters) | |
| 144 | | `GET …/packages/<type>/<name>` | one package | |
| 145 | | `GET …/packages/<type>/<name>/versions` | its versions, newest first, with their tags in `metadata.container.tags` | |
| 146 | | `GET …/packages/<type>/<name>/versions/<id>` | one version | |
| 147 | | `DELETE …/packages/<type>/<name>/versions/<id>` | delete a version and its tags | |
| 148 | | `DELETE …/packages/<type>/<name>` | delete the package | |
| 149 | | `PATCH …/packages/<type>/<name>` | Diffinery only: `{"visibility": "public"}` or `{"repository": "api"}` (`null` unlinks) | |
| 150 | | `GET /repos/<owner>/<repo>/packages` | Diffinery only: packages linked to a repository | |
| 151 | |
| 152 | The registry itself is at `/v2/`: blob uploads (monolithic, chunked and |
| 153 | streamed), cross-repository mounts, range requests, image manifests and |
| 154 | indexes (OCI and Docker v2), tag listing with pagination, `DELETE` of |
| 155 | manifests and tags, the referrers API (signatures and SBOMs attached with |
| 156 | `subject`), and `/v2/_catalog` (the repositories you can pull). |
| 157 | |
| 158 | ## Size limits |
| 159 | |
| 160 | Cloudflare limits one request to 100 MB, and `docker push` sends each layer |
| 161 | in a single request. On Diffinery's hosted runners this doesn't matter: the |
| 162 | runner's Docker daemon (and BuildKit builders created with |
| 163 | `docker buildx create` or `docker/setup-buildx-action`) reach the registry |
| 164 | through a local relay that splits uploads into smaller chunks. Layers of any |
| 165 | size push from workflows that use `docker` or BuildKit. Other clients run |
| 166 | directly in a step (`crane`, `skopeo`) don't go through the relay. |
| 167 | |
| 168 | From a laptop or a self-hosted runner, a layer over 100 MB fails to push with |
| 169 | `413` or a reset connection, because the common clients (`docker`, `buildx`, |
| 170 | `crane`, `skopeo`) send a layer in one request. The registry accepts chunked |
| 171 | uploads (`PATCH` with `Content-Range`) of any total size, so a client that |
| 172 | chunks works. Otherwise push from a hosted workflow, or keep layers under |
| 173 | 100 MB by putting large dependencies in their own `RUN`/`COPY` steps. Pulls |
| 174 | have no size limit. |
| 175 | |
| 176 | ## Cleanup |
| 177 | |
| 178 | Unfinished uploads are discarded after a day. Deleting a version removes its |
| 179 | tags; layers no other image uses are removed from storage by a scheduled job. |