Skip to content
264 linesCodeBlameRaw
1---
2title: Container images
3description: Push and pull container images on g1t.sh with docker, in workflows with G1T_TOKEN, and push layers over 100 MB with g1t push.
4---
5
6g1t.sh is a container registry. Images are named after their workspace,
7pushed and pulled with `docker` or any client of the OCI Distribution
8protocol, and have the access of the repository they are linked to (see
9[who can see and publish a package](/guides/packages/#who-can-see-and-publish-a-package)).
10
11```text
12g1t.sh/<workspace>/<name>[:<tag>]
13```
14
15## Sign in
16
17Use your username, and an [access token](https://g1t.sh/settings/tokens) as
18the password: one with full access, or with `packages:write` (to push) or
19`packages:read` (to pull private images).
20
21```sh
22echo "$G1T_TOKEN" | docker login g1t.sh -u <you> --password-stdin
23```
24
25Public images pull without signing in.
26
27## Push
28
29Tag the image with its address and push it:
30
31```sh
32docker build -t g1t.sh/acme/web:1.4.0 .
33docker push g1t.sh/acme/web:1.4.0
34```
35
36The first push makes the package. When its name starts with a repository
37of the workspace, here `acme/web`, it is linked to that repository and
38follows its visibility and roles; pushing needs Write on it. Otherwise it is
39the workspace's, private, and needs the workspace's Write base permission.
40
41### Link an image with its source label
42
43An image can name the repository it is built from, whatever the image is
44called, with the `org.opencontainers.image.source` label:
45
46```dockerfile
47LABEL org.opencontainers.image.source=https://g1t.sh/acme/web
48```
49
50or as an annotation on the manifest
51(`docker buildx build --annotation "org.opencontainers.image.source=https://g1t.sh/acme/web"`).
52g1t reads the manifest's annotations first, then the image config's labels.
53When the address is a repository of the image's own workspace
54(`https://g1t.sh/<workspace>/<repo>`, with or without `.git`) and whoever
55pushes has the Write role on that repository, the image is linked to it:
56
57| When | What happens |
58| --- | --- |
59| The image's first push | It is linked to the repository the label names, in place of the one its name starts with. |
60| A later push of an image that is not linked | It is linked to the repository the label names. |
61| A later push of a linked image | Nothing changes. Link it elsewhere from its [settings](/guides/packages/#package-settings). |
62| The label names another workspace's repository, another host, or a repository you cannot write to | It is ignored, and the image is linked by its name, as above. |
63
64Linking gives the image the repository's visibility and, unless its
65admins turn inheriting off, its roles. The link is recorded in the
66[audit log](/guides/audit-log/) as `package.linked`.
67
68Pushing a tag again moves it to the new image. Layers already on g1t are
69not uploaded again, and images in the same workspace share them.
70
71Multi-platform images (`docker buildx build --platform linux/amd64,linux/arm64 --push`),
72OCI image indexes, and artifacts attached to an image with a `subject`
73(signatures, SBOMs, attestations) are all kept; the registry lists an image's
74attached artifacts at `/v2/<name>/referrers/<digest>`.
75
76## Pull
77
78```sh
79docker pull g1t.sh/acme/web:1.4.0
80docker pull g1t.sh/acme/web@sha256:…
81```
82
83## In workflows
84
85A workflow's `G1T_TOKEN`, [the job's own token](/guides/actions/#the-jobs-token), pulls and pushes the images
86linked to its own repository, and with `packages: write` in its
87[`permissions:`](/guides/actions/#the-jobs-token) pushes new ones. An image
88linked to another repository, or one of the workspace's own that the job
89did not make, needs that job's repository added under the image's
90[Manage Actions access](/guides/packages/#manage-actions-access):
91
92```yaml
93jobs:
94 image:
95 runs-on: ubuntu-latest
96 permissions:
97 contents: read
98 packages: write
99 steps:
100 - uses: actions/checkout@v4
101 - name: Sign in to g1t.sh
102 run: echo "${{ secrets.G1T_TOKEN }}" | docker login g1t.sh -u g1t --password-stdin
103 - name: Build and push
104 run: |
105 docker build -t g1t.sh/${{ github.repository }}:${{ github.sha }} .
106 docker push g1t.sh/${{ github.repository }}:${{ github.sha }}
107```
108
109On g1t's own machines a job is already signed in to g1t.sh with that
110token when it starts, so the sign-in step can be left out there; it does
111no harm. Pushing needs `packages: write` in the job's
112[`permissions:`](/guides/actions/#the-jobs-token); pulling needs only the
113default `packages: read`. Runs that get no secrets (a pull request from
114someone without Write) get a token that only reads: they pull, and cannot
115push. See
116[secrets and variables](/guides/actions/#secrets-and-variables).
117
118## The 100 MB limit
119
120A single request to g1t.sh may carry at most 100 MB. `docker push` sends
121each layer whole, in one request, so a layer over 100 MB, as compressed for
122the push, is refused. [`g1t push`](#push-large-layers-with-g1t-push) sends
123it in chunks instead, and has no limit.
124
125What you see depends on where it is refused. Usually it is before the
126request reaches g1t, and `docker push` stops with a bare status:
127
128```text
129unknown: failed commit on ref "layer-sha256:…": unexpected status from PUT request to https://g1t.sh/v2/acme/web/blobs/uploads/…?digest=sha256%3A…: 413 Request Entity Too Large
130```
131
132(the request may be a `PATCH` instead of a `PUT`, and some versions of
133Docker show the HTML page that came with the 413 instead). When g1t sees
134the request itself, the error is `SIZE_INVALID`, with a message naming the
135limit and this page.
136
137An installation you [run yourself](/guides/self-hosting/) has no such
138limit.
139
140### Push large layers with g1t push
141
142`g1t push` takes an image from your local docker and pushes it to g1t.sh,
143each layer in chunks of 90 MiB, each chunk its own request. A layer can be
144any size.
145
146```sh
147docker build -t g1t.sh/acme/model:1 .
148g1t push g1t.sh/acme/model:1
149```
150
151An image with a local name is pushed to the address after `--as`:
152
153```sh
154g1t push model:dev --as g1t.sh/acme/model:1
155```
156
157```text
158Reading model:dev from docker
159Pushing to g1t.sh/acme/model:1
160 config sha256:040e744c070b 851 B already on the registry, skipped
161 layer sha256:25f1d6b1951a 3.5 MiB already on the registry, skipped
162 chunk 1/2 90.0 MiB 63%
163 chunk 2/2 53.1 MiB 100%
164 layer sha256:c74595c4a2cd 143.1 MiB uploaded in 2 chunks
165Pushed g1t.sh/acme/model:1
166digest: sha256:39b972d91774d58b5fbd27cfdce73fe58034d9e5772a261d183c11bcf78c1dba
167```
168
169It pushes the image docker has: the same config, layers and manifest, so
170`docker pull` gets back exactly what you built. When docker keeps a layer
171uncompressed, `g1t push` gzips it for the push, as `docker push` does.
172Layers already on g1t are not sent again. A request refused with `429` or
173an error on g1t's side is tried again after a wait, and an interrupted
174layer goes on from where it stopped.
175
176It signs in with the first of:
177
1781. a token on stdin, with `--token-stdin`
179 (`echo "$G1T_TOKEN" | g1t push … --token-stdin`);
1802. the `G1T_TOKEN` environment variable, as in [workflows](#in-workflows);
1813. what `docker login g1t.sh` stored, in `~/.docker/config.json` or the
182 credential store it names.
183
184| Option | |
185| --- | --- |
186| `--as <address>` | Where to push, `g1t.sh/<workspace>/<name>:<tag>`. Without it, the image's own name must be such an address. The tag defaults to `latest`. |
187| `--chunk-size <size>` | How much each request carries: `5MB` to `95MB`, `90MB` unless set. `MB` and `MiB` both mean 1,048,576 bytes. |
188| `--token-stdin` | Read the token from stdin. |
189| `--archive <file>` | Push a tarball written by `docker save`, instead of asking docker. Needs `--as`. |
190
191`g1t --version` prints its version, and `g1t help` its options.
192
193### Getting g1t
194
195One file for each platform, from `https://g1t.sh/downloads/cli/latest/`:
196
197```sh
198# Linux (x64; use g1t-linux-arm64 on ARM)
199curl -fsSLo g1t https://g1t.sh/downloads/cli/latest/g1t-linux-x64 && chmod +x g1t
200# macOS (Apple silicon; use g1t-macos-x64 on Intel)
201curl -fsSLo g1t https://g1t.sh/downloads/cli/latest/g1t-macos-arm64 && chmod +x g1t
202```
203
204```powershell
205# Windows
206Invoke-WebRequest https://g1t.sh/downloads/cli/latest/g1t-windows-x64.exe -OutFile g1t.exe
207```
208
209Check a download against `https://g1t.sh/downloads/cli/latest/SHA256SUMS`.
210To build it from source instead, with
211[Rust](https://www.rust-lang.org/tools/install):
212
213```sh
214git clone https://g1t.sh/flagon-io/g1t
215cd g1t
216cargo install --path crates/g1t
217```
218
219## Storage and pull limits
220
221Without the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), a
222workspace's public packages may hold 10 GB and its private ones 500 MB,
223each file counted once. A push that would go past either is refused with
224`DENIED` and a message saying how much is used; layers the workspace
225already holds add nothing. On the plan nothing is refused: storage past
226the free amounts is charged.
227
228Anonymous pulls are limited to 300 requests a minute from each address, and
229signed-in ones to 5,000 a minute for each person, workspace or agent.
230Past the limit, requests are answered `429` with `TOOMANYREQUESTS` and a
231`Retry-After`; docker waits and tries again. Signing in raises the limit.
232
233## Delete
234
235Deleting needs the Admin role on the image (Admin on the linked repository
236while it inherits access, a role given on the image itself, or an owner of
237the workspace); a token needs `packages:delete`.
238
239The registry protocol's `DELETE` removes a tag, or a whole version by its
240digest (with every tag that points to it). A deleted version can be
241[restored](/guides/packages/#delete-and-restore) for 30 days, and until
242then a manifest with its digest cannot be pushed again:
243
244```sh
245TOKEN=$(curl -s -u <you>:<token> "https://g1t.sh/v2/token?scope=repository:acme/web:delete" | jq -r .token)
246curl -X DELETE -H "Authorization: Bearer $TOKEN" https://g1t.sh/v2/acme/web/manifests/1.4.0
247curl -X DELETE -H "Authorization: Bearer $TOKEN" https://g1t.sh/v2/acme/web/manifests/sha256:…
248```
249
250A deleted version keeps its layers until it is purged. Layers no version
251uses any more are deleted from storage a day later.
252
253## Errors
254
255| Error | Means |
256| --- | --- |
257| `UNAUTHORIZED` | Not signed in, or the token is wrong or expired. `docker login g1t.sh` again. |
258| `DENIED` | Signed in, but your role or your token's scopes do not allow it. The message says which. |
259| `NAME_UNKNOWN` | No such image, or one you cannot see. |
260| `MANIFEST_UNKNOWN`, `BLOB_UNKNOWN` | No such tag, digest or layer in that image. |
261| `NAME_INVALID` | Names are lowercase letters and digits, separated by `.`, `_`, `__`, `-` or `/`, and start with a workspace. |
262| `SIZE_INVALID`, or a bare `413` | A request over [the 100 MB limit](#the-100-mb-limit). Push the image with [`g1t push`](#push-large-layers-with-g1t-push). |
263| `TOOMANYREQUESTS` | Too many requests in a minute; see [the limits](#storage-and-pull-limits). |
264| `DIGEST_INVALID` | What was uploaded does not have the digest the client said. Push again. |