Deploy a Docker image
Build the image wherever you already build images — your laptop, your CI — push
it to Ghayma, and deploy it. Nothing is built on the platform: the image you
pushed is the image that runs. An image deploy is an ordinary deploy — the same
limits, the same deployment history and the same rollbacks as ghayma deploy.
No registry account anywhere. The push goes to Ghayma’s own registry and signs in with your Ghayma identity — there is no Docker Hub or ghcr account to create, and no registry password to store. Only the layers the registry does not already have are uploaded, so a second push of the same app is a small one.
Push and deploy
Build the image
docker build --platform linux/amd64 -t my-app .--platform linux/amd64 matters on an Apple laptop — see the image
contract below.
Push it
ghayma docker push my-app --tag v1The CLI works out which app the image belongs to, mints a push credential for
that one repository, and hands the upload to your own docker:
🐳 Pushing my-app → registry.ghayma.tech/my-app:v1
⬆️ Uploading (only the layers the registry doesn't have)...
✅ Pushed registry.ghayma.tech/my-app:v1
digest sha256:0a1b2c…
Deploy it with: ghayma deploy --image v1Deploy it
ghayma deploy --image v1Nothing is built. The image is checked, then rolled out as it is.
ghayma docker push runs the docker CLI — it is what built the image, and
its layer-existence checks are what make a repeat push upload only what changed.
A machine without it is told so before anything else happens. Its flags:
| Flag | What it does |
|---|---|
--site | Site whose repository to push to (name or slug) |
--tag | Tag to push as (default: the local image’s tag, else latest) |
--deploy | Deploy the image once it is pushed |
--prod | With --deploy, deploy to production |
The credential the push logs in with is minted for that one repository, expires in about 30 minutes, and is dropped again when the push finishes — whether it succeeded or not. Nothing to rotate and nothing left behind.
See ghayma docker push for the full command
reference.
Pushing before your first deploy
A push does not need a site. A project created with no site — or one you have never deployed — still has a repository waiting at its own name, so you can push an image first and decide what to do with it after:
✅ Pushed registry.ghayma.tech/my-app:v1
digest sha256:0a1b2c…
ℹ️ This project has no site yet, so there is nothing to deploy the image to.
Create one with: ghayma site create main
Then deploy it with: ghayma deploy --image v1Deploying, on the other hand, replaces what an app is serving, so it needs one.
ghayma deploy --image on a site-less project says which two ways there are to
get a site, rather than picking one for you:
ℹ️ This project has no site yet, and deploying an image needs one.
Create it with: ghayma site create main
(or run 'ghayma deploy' once to deploy from source, which creates the main site)ghayma docker push --deploy says the same thing — before the upload, not
after it.
The image contract
Five things are asked of the image. Four of them are checked before anything is scheduled, and a deploy that fails one of them fails with the reason in its build log; the fifth is your app answering its health check.
-
Built for
linux/amd64. Every Ghayma node islinux/amd64, and a plaindocker buildon an Apple Silicon machine producesarm64. Pass--platform linux/amd64, or push a multi-arch image that contains anlinux/amd64variant. -
A numeric, non-root
USER.USER 1000,USER 101,USER 65532— a number, not a name. NoUSERat all,USER rootandUSER 0are refused, and so isUSER app: the platform enforces “never as root” through the kubelet, which can only verify a uid. Unlike an image Ghayma builds for you, your image’s own uid is what runs — songinxinc/nginx-unprivileged(uid 101) works as it is, and nothing re-owns the paths your Dockerfile chowned. -
Listens on
$PORT, bound to0.0.0.0.PORTis injected from the site’s port setting (default3000). Bind the interface, notlocalhost— a server on127.0.0.1is unreachable from outside its own container. If your image declares oneEXPOSEd port and the site has no port set, that port is adopted as the site’s automatically; twoEXPOSEs are ambiguous and neither is taken, so set the port yourself in the site’s build settings. -
Answers
GETon the health path with a 2xx or 3xx. The default path is/, changeable per app under the site’s Settings → Health path. The readiness check starts 10 seconds in and repeats every 5; until it passes, the rollout does not finish and the previous build keeps serving. -
Within your plan’s maximum image size. Measured exactly — the config and layers of the
linux/amd64variant — against the app tier’s ceiling. See Pricing by service.
NODE_ENV=production and HOSTNAME=0.0.0.0 are injected into every app pod,
image deploys included. Harmless for a non-Node image, and what a Node one
usually wants.
What a deploy does with your tag
--image takes a tag, or a sha256: digest for one exact image:
ghayma deploy --image v1
ghayma deploy --image v1 --site dev
ghayma deploy --image sha256:0a1b2c…An image deploy is a production one when the site it targets is a
production site, like every other deploy; --prod is
accepted and no longer decides anything.
The tag is resolved to a digest at admission, and that digest is what is
rolled out, recorded and rolled back to. Your tag can move afterwards; the
deployment doesn’t. Push a new image over :v1 tomorrow and yesterday’s
deployment still points at yesterday’s bytes.
By the same rule, deploying v1 twice creates two deployments and deliberately
re-resolves the tag each time — so the history says what actually ran, and never
what the tag happens to mean today.
When it is accepted, the deployment’s build log opens with exactly what was admitted:
Image my-app:v1 @sha256:0a1b2c… (142 MB, linux/amd64) acceptedand the deployment appears on the Deployments tab with an image badge,
the repository:tag it resolved from, and the short digest that ran.
Everything else about it is an ordinary deploy: it waits its turn in the build queue, it counts against your plan’s daily deployment limit, a refused or failed image leaves the previous build serving, and you can roll back to it or from it — including back and forth between an image deploy and a source deploy.
Environment variables are still yours
Runtime environment variables work exactly as on every other deploy: the site’s own variables, plus the credentials the platform derives for you — databases, buckets, auth apps, cron secrets — are assembled and injected at deploy time. See Environment Variables.
Build-time variables are not. A variable marked build-time is read by
Ghayma’s builder while it builds your source, and an image deploy has no build.
Anything baked into a bundle — NEXT_PUBLIC_*, VITE_*, and their equivalents —
is decided on the machine that ran docker build, so pass it there:
docker build --platform linux/amd64 \
--build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
-t my-app .Setting such a variable on the site changes nothing about an image that was already built with (or without) it.
Cron jobs are not synced
The crons array lives in .ghayma.json and is applied by ghayma deploy
reading that file on your machine. An image deploy carries no .ghayma.json, so
it never adds, changes or removes a cron job — exactly like a git
deploy.
Cron jobs you already have keep running, and keep hitting the newly deployed
image with their CRON_SECRET_* signatures as before. To change the schedule
list itself, run ghayma deploy from the source directory. See Cron
Jobs.
From the dashboard
The site’s Deployments tab carries a Pushed images panel listing what you have pushed to that app’s repository — the tag, its digest, its size, when the image was built, and whether a deployment already used it — with a Deploy button per tag. Ghayma’s own internal deployment tags are not listed; they are not something anyone would choose.
How long a pushed image is kept
A tag whose digest some deployment used is kept for as long as that deployment is. A tag you pushed and never deployed is kept for 30 days and then pruned, so abandoned pushes do not accumulate against the platform forever. Push it again whenever you want it back.
When a deploy is refused
An image that fails a check is a failed deployment whose build log says why, in these words:
| What you see | What to do |
|---|---|
This image has no linux/amd64 variant, and every Ghayma node is linux/amd64. Rebuild it with docker build --platform linux/amd64 and push again. | Rebuild with --platform linux/amd64 (or build a multi-arch image) and push again. A plain docker build on Apple Silicon is the usual cause |
This image is linux/arm64, and every Ghayma node is linux/amd64. Rebuild it with docker build --platform linux/amd64 and push again. | The same fix — the message names what you actually pushed. A plain docker build on Apple Silicon lands here |
This image runs as root. Declare a numeric non-root UID in your Dockerfile's USER (for example USER 1000) and push again — Ghayma never runs an app pod as root. | Add a USER line with a number. No USER, USER root and USER 0 all land here |
This image declares a named user, which the kubelet cannot verify as non-root. Declare a numeric UID in your Dockerfile's USER (for example USER 1000) and push again. | Replace USER app with the uid that user has — USER 1000 |
image exceeds tier limit; store large assets in object storage (image 412MB > tier s limit 350MB) | Slim the image, or move the bulk into object storage. Multi-stage builds and a smaller base are the fastest wins |
And for the push itself:
| What you see | What to check |
|---|---|
'ghayma docker push' runs the docker CLI, and no 'docker' was found on PATH. | Install Docker, or start Docker Desktop. The push is docker’s own upload; there is no daemon-free mode yet |
| A push that stops part-way with an authentication error | The push credential lasts about 30 minutes. Run the same ghayma docker push again — it mints a fresh one, and the layers that already arrived are not re-uploaded |
could not tag my-app: … | The local image name is wrong. docker images shows what you actually built |
| A deploy that stays deploying and then fails | Readiness never passed. Check that the app binds $PORT on 0.0.0.0, and that GET on the site’s Health path answers 2xx/3xx — an app that answers 404 at / needs its health path set to something it serves |
A refusal happens before anything runs, and a failed deploy of any kind leaves the previous build serving. Nothing you push can take a live app down by being wrong.
Related
- Deploy & Manage —
ghayma docker push,ghayma deploy, rollbacks - Deploy from GitHub — the other way to deploy without uploading
- Builds — the queue, the limits, and why a deploy fails
- Custom Dockerfile — keep building on Ghayma, from your own Dockerfile
- Environments — promoting an image from one environment to another
- Pricing by service — app tiers and the maximum image size