Deploy & Manage
ghayma init
Initialize a new project in the current directory. Creates the project on
Ghayma and writes a .ghayma.json config file.
ghayma initYou will be prompted for:
- Project name — becomes your slug and the default
<slug>.ghayma.appdomain - Framework — auto-detected from your project files; there’s no selector. Init prints
🔎 Detected framework: <fw>, where<fw>is the CLI’s detected label —nextjs,node,go,python,php,ruby,rust, orstatic, orautowhen nothing matches (see Supported frameworks). - Plan — an interactive picker over the plans available to you, each line showing its monthly price and points budget. Skip it with
--plan <slug>. See Choosing a plan. - How you’ll use this project — deploy an app or site from this directory, or no site at all: databases, storage and auth only. See Projects without a site.
- Site name — only when you deploy a site: leave empty for
main(default), or type a name likefrontendoradmin - Domain — only when you deploy a site: optional custom domain to attach right away
Choosing a plan
ghayma init asks which plan to create the project on. Every plan carries a
points budget you spend across databases, apps, storage, and auth apps — see
Points & Plans. Pass --plan <slug> to skip the
picker (handy for scripts and CI):
ghayma init --plan proThe interactive picker lists the plans available to you with their price and budget:
Select a plan for this project
▸ hobby — 1,000 DZD/mo · 9 pts
pro — 5,000 DZD/mo · 35 ptsAn unknown --plan slug is rejected up front, listing the valid slugs. If the
plan catalog can’t be reached, an explicit --plan is still sent and validated
by the server — so ghayma init --plan pro never silently lands on the default
plan; without --plan, init falls back to the server’s default plan.
Prices and budgets come from the live catalog — the figures above are only an example.
Projects without a site
Not every project deploys something. A mobile app, a desktop app or a backend you host yourself uses a Ghayma project for its databases, buckets and auth app alone — so init asks:
How will you use this project?
▸ Deploy an app or site from this directory
No site — only databases, storage and auth (mobile app, external backend)The second answer skips the site and domain questions and writes a
.ghayma.json with no site_* fields:
✅ Project 'my-app' created (slug: my-app) — no site
📁 Config saved to .ghayma.json
Next: ghayma db create <name> ghayma storage create <name> ghayma auth create <name>
Add a site later with: ghayma site create <name>db, storage, auth, points and project then work exactly as they do in
any other project. The commands that need a site — deploy, env, domain,
logs, rollback, cron, site use|scale — say so instead of failing:
ℹ️ This project has no site yet. Create one with: ghayma site create <name>.
Add one whenever you like with ghayma site create, or just run
ghayma deploy: it still creates main for you, and says so before it starts.
For scripts and CI:
ghayma init --no-site # no site, and no question about it
ghayma init --site frontend # that site, no site question
ghayma init --site main --domain app.example.com--no-site together with --site or --domain is refused before the project
is created.
Monorepo initialisation
Init recognises a workspace root — a directory that declares workspace
members: a turbo.json, a pnpm-workspace.yaml with a packages: list, or a
package.json with a workspaces key — and asks for an app subdirectory
before running the prompts above:
📦 Workspace root detected (pnpm-workspace.yaml).
The app config should live in your app's subdirectory so the deploy
uploads the whole workspace and builds the right target.
App subdirectory (e.g. apps/web; leave empty to init at root):A pnpm-workspace.yaml that only carries settings — pnpm 10 writes one into a
single-package project for ignoredBuiltDependencies, so a plain
create-next-app has one — declares no packages and is not a workspace: a
single app is linked from its own directory and this question is never asked.
The default answer is the workspace’s first app directory, or apps/web when
it has none yet. Your answer must be a directory that exists and holds a
package.json — a wrong answer is simply asked again (three tries), never
fatal. A Next.js app/ router folder is not an app directory. Leave the answer
empty to initialise at the root instead.
When you give a subdir, init writes .ghayma.json inside that subdir and
records root_directory in the config so subsequent ghayma deploy runs from
either the monorepo root or the app subdir build the right target.
init links one app per run. To link every app in a workspace at once —
one manifest at the root, one site per app directory — use
ghayma link instead; see
Workspaces (monorepos).
See Sites for how sites work across a monorepo.
.ghayma.json
{
"project_id": "uuid",
"name": "my-app",
"slug": "my-app",
"framework": "nextjs",
"site_name": "main",
"root_directory": "apps/web"
}| Field | Description |
|---|---|
project_id | Unique project identifier |
name | Human-readable project name |
slug | URL-safe slug used in the default <slug>.ghayma.app domain |
framework | Framework tag written at init, auto-detected from your project files (auto when undetermined) |
site_id | ID of the active site. Empty for a brand-new main site until the first deploy creates it |
site_name | Display name of the active site |
site_slug | Slug of the active site |
root_directory | Monorepo only: path from monorepo root to the app being deployed (e.g. apps/web) |
upload | Workspace manifest only: "app" uploads just root_directory, "workspace" uploads the whole workspace and builds root_directory inside it |
install_command | Override the detected install command |
build_command | Override the detected build command |
start_command | Override the detected start command |
output_directory | Static builds: directory your build emits into (e.g. dist) |
port | Port your app listens on (default 3000; 80 for static) |
dockerfile_path | Path to your own Dockerfile, relative to the app’s directory (root_directory) — see Custom Dockerfile |
builder | "auto" to always generate the Dockerfile for this site and ignore any Dockerfile in the repo. Leave it out for the default behaviour |
The same filename does double duty in a workspace repo: a .ghayma.json at
the workspace root that carries a sites array is a manifest describing
every site in the project at once, each entry taking the per-site fields above.
See Workspaces (monorepos).
For single-app projects you can ignore the site_* fields entirely. A new
project’s main site is created by the platform on your first deploy, so
init writes site_id / site_slug empty until then — but only while the
project has no sites at all. A project without a
site carries no site_* field at all. You only
need to think about sites when you have multiple deployable apps in one
project. See Sites.
Where build settings live
The build fields are optional overrides — leave them out and the platform auto-detects. They belong to one site, not to the whole project: two sites in the same project can run different frameworks, commands and ports, and editing one never touches the other.
At deploy time the platform reads them in this order:
.ghayma.json— the per-site keys above, or the site’s entry in a workspace manifest- the settings saved on that site (Dashboard → the site → Settings)
- auto-detection
A deploy also writes back every build field it finds in .ghayma.json
(including dockerfile_path and builder) onto that site, so they become the
site’s standing defaults until you change them.
Deleting a key from .ghayma.json does not clear the saved value — the
last value you deployed stays in effect. Clear it in the site’s Settings
instead (an empty field, or 0 for the port, returns that setting to
auto-detection).
Supported frameworks
ghayma init picks the framework label for .ghayma.json from the files in
your project directory:
| Label | Detected from |
|---|---|
nextjs | package.json containing next |
node | any other package.json |
go | go.mod |
python | requirements.txt or pyproject.toml |
php | composer.json |
ruby | Gemfile |
rust | Cargo.toml |
static | index.html and no package.json |
auto | nothing matched — the platform decides at build time |
package.json is checked before index.html, so a Node app that ships a static
index.html is not mis-detected as static.
Every image is built the same way — by a rootless BuildKit job in the
cluster, the same engine behind docker build. What the label decides is the
build plan that job runs:
| Build plan | Used for | Notes |
|---|---|---|
| Generated Dockerfile | Node projects — nextjs, nuxt, react, node | Next.js builds with npm run build (npx turbo run build --filter=<app> in monorepos) and starts with node server.js from the standalone output. Others run npm run build when a build script exists and start from the start script, else node index.js. Nuxt and React are refined server-side from your dependencies |
| Generated nginx image | static, and any repo with no package.json that has an index.html | Gzip, caching headers, and SPA fallback to index.html. Set output_directory when your build emits to dist/build |
| Railpack | python, go, php, ruby, rust, java, deno, elixir, and any repo with no package.json | Railpack auto-detects the language and its build/start commands |
A Dockerfile in your repo overrides all of the above once custom Dockerfiles
are enabled for the project — unless that site sets "builder": "auto", which
always generates the Dockerfile and ignores yours. See Custom
Dockerfile.
For Node builds, the install command is chosen from the lockfile at the repo
root: pnpm-lock.yaml → corepack enable && pnpm install --frozen-lockfile,
yarn.lock → yarn install --frozen-lockfile, bun.lockb →
bun install --frozen-lockfile, package-lock.json → npm ci, and
npm install when there is no lockfile at all.
ghayma link
Connect the current directory — or a whole workspace — to a project that
already exists on your account. Use this instead of init when cloning a repo
on a new machine.
# Pick from an interactive list
ghayma link
# Connect directly by slug or name
ghayma link my-projectAfter picking a project (and site, when the project has more than one),
link writes the same .ghayma.json that init would — no new
project is created.
The site list carries two fixed choices alongside the project’s own sites: ➕
Create a new site and — No site (databases, storage and auth only). The
second writes the config without site fields, exactly like init without a
site; ghayma link --no-site picks it without
asking.
Linking a whole workspace
At a workspace root — a directory that declares workspace members:
turbo.json, a pnpm-workspace.yaml with a packages: list, or a
package.json with a workspaces key — link first asks how much of it you
want to link:
How do you want to link this workspace?
▸ Whole project — every site, one manifest at ./.ghayma.json
One app subdirectory — writes apps/<dir>/.ghayma.jsonOne app subdirectory is the previous behaviour, unchanged: one site, one
.ghayma.json inside that app’s directory, one app per run.
Whole project (recommended for monorepos) fetches the project’s sites and maps each one to a directory for you:
- a directory whose existing
.ghayma.jsonalready carries that site’ssite_id - a directory whose name equals the site’s slug or name
- otherwise it asks —
Which directory holds site "admin"?— with a skip option; skipped sites are left out of the manifest
Build settings found in an existing per-app file are carried into that site’s
entry, and the per-app file is left in place: it keeps working, and you can
delete it whenever you like. link finishes by printing what it mapped, one
line per site — site → directory (upload):
main → apps/taarefni (app)
admin → apps/taarefni-admin (app)Re-run link at a root that already has a manifest and it tells you the
workspace is already linked. Both modes need an interactive terminal.
A settings-only pnpm-workspace.yaml — the one pnpm 10 writes into a
single-package project for ignoredBuiltDependencies — declares no packages,
so it is not a workspace root: link in a plain single app links that
directory straight away, with no mode question.
Workspaces (monorepos)
Link a workspace once and every app inside it is a site of the same project — no per-app config files to keep in sync, and no switching directories to deploy a different app.
Link the whole project
cd my-monorepo
ghayma link taarefni # or plain `ghayma link` and pick from the listThis writes one .ghayma.json at the workspace root — a manifest listing
every site and the directory it builds from:
{
"project_id": "…",
"name": "taarefni",
"slug": "taarefni",
"sites": [
{
"site_id": "…",
"site_name": "main",
"site_slug": "main",
"root_directory": "apps/taarefni",
"upload": "app",
"framework": "nextjs"
},
{
"site_id": "…",
"site_name": "admin",
"site_slug": "admin",
"root_directory": "apps/taarefni-admin",
"upload": "app"
}
]
}root_directory is the app’s path from the workspace root, always with
forward slashes. Every per-site field of a single-app config — framework,
dockerfile_path, build_command, install_command, start_command,
output_directory, port, crons — is allowed inside an entry and means
exactly the same thing there: settings belong to the site, so each entry
configures its own build.
builder is the one exception for now — the API accepts it, but the released
CLI doesn’t carry it in a manifest entry yet. Set Always generate the
Dockerfile for this site in the site’s Settings until the next CLI release.
Deploy from anywhere in the workspace
# From the root — pick a site from the list
ghayma deploy
# From the root — name the site
ghayma deploy --site admin
# From the app's own directory — implied, nothing to pick
cd apps/taarefni-admin && ghayma deployPin the site in CI
ghayma deploy --site adminA shell with no TTY is never prompted: at a manifest root with several sites
and no --site, the deploy fails with
several sites in this workspace — pass --site <slug> (available: main, planify, …).
What gets uploaded
Each entry’s upload decides what the CLI ships:
upload | The CLI uploads | Build runs in |
|---|---|---|
"app" | only root_directory | the root of the upload |
"workspace" | the whole workspace | root_directory inside it |
link writes "workspace" only when the root has a turbo.json, and "app"
otherwise — which is exactly what each kind of repo already did, so linking a
workspace never changes how a live build runs. Change it per site by editing
the entry.
The deploy headline says which one is on the wire:
🚀 Deploying taarefni [site: admin] (uploading apps/taarefni-admin only)...
🚀 Deploying taarefni [site: web] (uploading the workspace, building apps/web)...Other commands in a workspace
ghayma env set|list|delete|importandghayma domain createresolve the site the same waydeploydoes: implied inside an app directory, then--site <slug>or a picker at the manifest root, and an error on a non-interactive shell. See Environment Variables and Domains.- Project-scoped commands —
db,storage,auth,points,logs,rollback,site list|create|scale,cron,project— find the nearest config by walking up from the current directory, so they work from anywhere inside a linked workspace. ghayma site createat a linked workspace offers to add the new site to the manifest, asking which directory holds it (you can skip).ghayma site useis refused at a manifest root — there is no single active site there. See Sites.
ghayma deploy
Deploy the current project. Reads .ghayma.json to determine the
project and the site, packages source code, builds a container, and
starts it.
ghayma deployEvery deploy replaces what the target site is serving. A deploy is a production one exactly when the site it targets is a production site — that is what the Production tag in the deployment history now means.
--prod is still accepted and no longer decides anything. Pass it and the CLI
says so before the upload starts:
ℹ️ --prod no longer changes anything: whether a deploy is production now follows the target site's environment (main is production).To deploy somewhere that is not production, target a
staging or development site
— ghayma deploy --site dev — rather than dropping the flag.
Which site gets deployed depends on where you run the command:
| Run from | Site deployed |
|---|---|
An app directory with its own .ghayma.json | The site in that file — no question asked |
An app directory whose .ghayma.json names no site (written before configs carried one) | --site <slug> when you pass it, checked against the project’s sites before anything is uploaded; otherwise the project’s default site |
| A manifest root | --site <slug>, or the only site, otherwise a picker listing slug (directory) |
| A directory listed in the manifest, or anything below it | That entry (nearest listed ancestor) — no question asked |
--site accepts a slug, a name, or an id, case-insensitively, and is how you
deploy from CI:
ghayma deploy --site adminNothing is ever guessed at:
- no TTY and several sites →
several sites in this workspace — pass --site <slug> (available: main, planify, …) - a name that isn’t in the manifest →
unknown site "x" in this workspace (available: …) - a directory the manifest doesn’t list →
this directory is not listed in <root>/.ghayma.json — run 'ghayma link' from the workspace root to add it --sitenaming a site other than the one the current directory pins → refused--sitenaming a site the project doesn’t have →site "x" not found in this project (available: …), before anything is uploaded. CLI versions before v0.11.11 ignored--sitewhen the directory’s.ghayma.jsonnamed no site and deployed the project’s default site instead; update the CLI- a single-app
.ghayma.jsonwith an emptysite_idon a project that has several sites and no default → refused rather than guessed at, see When no site is named
See Sites.
Before the upload starts, the CLI prints the ignore rules it will apply:
📋 Baseline ignore: node_modules, .next, .git, .turbo, dist
Applying .ghaymaignore:
• apps/mobile/android/
• apps/mobile/ios/
• **/*.logSee Ignoring files from uploads below.
What happens during deploy:
Package
Source code is compressed into a tar.gz archive, honouring your ignore rules.
Upload
The archive is uploaded to the Ghayma API.
Build
A container image is built by a rootless BuildKit job in the cluster. Layers are cached between builds, so an unchanged install step is reused. The deployment waits in a queue first when build capacity is full — see Builds.
Deploy
The image is rolled out to Kubernetes with zero-downtime rolling update.
Route
Traffic is routed to <your-project>.ghayma.app — or
<your-project>-<site>.ghayma.app for any site other than main — plus any
custom domains. See Site URLs.
While it runs, the CLI prints each state the deployment passes through, and its place in the queue while it waits for a build slot:
⏸️ Queued, position 3 of 7....
🔨 Building.......................
🚀 Deploying.....
✅ Deployed successfully!Builds explains the queue, how paid plans are prioritised, the 20-minute build limit and the resources a build gets, and what to do if a build times out.
Services no site uses yet
Before the upload, ghayma deploy offers each database, bucket or auth app that
no site uses to the site it deploys — on a first deploy, the main site that
deploy creates. One question each, default Yes:
Database 'shop-db' isn't connected to any site. Connect it to 'main'?
▸ Yes
NoA Yes connects it before the build is queued, so that build already has its variables. A database that is still starting connects once it accepts connections:
⏳ Connecting database 'shop-db' to 'main' once it is ready to accept connectionsA No, a run without a terminal, or --no-connect connects nothing:
ghayma deploy --no-connectThe CLI then prints the command for each service:
ℹ️ Not connected to any site: database 'shop-db', bucket 'media'.
Connect with:
ghayma connect database shop-db --site main
ghayma connect bucket media --site main- Nothing is offered when it isn’t clear which site the deploy lands on —
several sites with neither a default nor
main. - Only source deploys ask;
--imagedeploys don’t. - Connecting needs write on the site. A project key never connects, and an
account token needs the
projectsscope. - If the services can’t be listed, the deploy says so in one line and carries on.
See How a service gets connected.
Deploying a pushed image
--image deploys an image you already pushed with ghayma docker push instead of uploading this directory: nothing is
built, the image is checked and rolled out as it is.
# A tag you pushed
ghayma deploy --image v1
# One exact image, whatever the tags say
ghayma deploy --image sha256:0a1b2c…The tag is resolved to a digest before anything runs, and that digest is what is
deployed — so your tag may move later without changing what this deployment
serves. --site works as it does on any other deploy, --prod is accepted and
decides nothing, and a project with no site yet is told how to get one rather
than having one made for it.
See Deploy a Docker image for the image contract and the refusals.
ghayma promote
Deploy the exact image another site is already running onto this project’s production site. Nothing is rebuilt: the source deployment’s image is the artifact you tested, and that is what ships.
# The default site (production) gets what 'dev' is running now
ghayma promote --from dev
# Promote into a site of your choosing
ghayma promote --from staging --site main
# Promote one specific deployment of the source site
ghayma promote --from dev --deployment 6f1c84a2-...Output:
ℹ️ Target: the project's default site 'main' (production) — pass --site <slug> to promote somewhere else.
🚀 Promoting dev → main (image sha256:0a1b2c3d4e5f)
📦 Promotion queued (deployment: 6f1c84a2-...)
⏳ Checking the image and rolling it out...Options:
| Flag | Description |
|---|---|
--from | Site to promote from, by name or slug (required) |
--deployment | Deployment of that site to promote (default: its current live one) |
--site | Site to promote into (default: the project’s default site) |
--message | Message to label the resulting deployment with |
The target’s own environment variables, connections, credentials, domains and tier apply — a promotion is an ordinary deploy of a known-good image, and the image checks re-run against the target before anything is scheduled. It then waits, narrates and reports exactly like every other deploy, and you can roll back to it or from it.
Promoting needs write on the target site; read on the source is enough. A site can never be promoted onto itself.
❌ dev has no live deployment to promote. Deploy it first, or name a deployment with "deployment_id".
Or promote a specific one: ghayma promote --from dev --deployment <id>See Environments for the whole workflow.
Ignoring files from uploads
The CLI excludes these directories from every upload, regardless of configuration:
node_modules, .next, .git, .turbo, distFor anything else, add an .ghaymaignore file at the repo root. It
supports the same syntax as .gitignore — patterns, trailing / for
directories, ** for recursive match, and ! for negation:
# Platform dirs we don't ship for a web-only deploy
apps/mobile/android/
apps/mobile/ios/
# CI/build artifacts
**/*.log
coverage/
# Secrets
.env.localIf no .ghaymaignore exists, the CLI falls back to .dockerignore
(in the same .gitignore-compatible syntax). Precedence is
.ghaymaignore → .dockerignore → none.
ghayma docker push
Push a locally built image to your app’s repository in the Ghayma registry, so
ghayma deploy --image can deploy it. Requires the docker CLI on
PATH — the push is docker’s own upload.
docker build --platform linux/amd64 -t my-app .
ghayma docker push my-app --tag v1
ghayma deploy --image v1🐳 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 v1| Flag | What it does |
|---|---|
--site <name> | Site whose repository to push to (name or slug) |
--tag <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 |
ghayma docker push my-app # pushes my-app:latest as :latest
ghayma docker push my-app:dev --tag v1 # pushes the local dev image as v1
ghayma docker push my-app --site admin
ghayma docker push my-app --tag v1 --deploy --prodWhich app it pushes to follows the same rules as ghayma deploy — this
directory’s .ghayma.json, its entry in a workspace
manifest, or --site. A project with no site yet
pushes to its main repository, so a first push can precede a first deploy.
Only the layers the registry does not already have are uploaded. The credential is minted for that one repository, lasts about 30 minutes, and is signed out again when the push ends — a push that is refused part-way is simply rerun.
--prod without --deploy is refused rather than ignored, and --deploy on a
project with no site is refused before the upload rather than after it.
See Deploy a Docker image for the image contract —
linux/amd64, a numeric non-root USER, $PORT on 0.0.0.0, the health path
and the image-size ceiling — and for every refusal message.
ghayma logs
View application logs from the running container.
# Last 100 lines (default)
ghayma logs
# Last N lines
ghayma logs --lines 500
ghayma logs -n 500ghayma rollback
Roll back to a previous successful deployment. Only the last 10 successful
deployments of a site can be restored — older ones are not offered. The command
lists the live deployments still available, then asks you to pick one:
📋 Recent deployments for my-project:
[1] 2026-04-21T16:30:12Z — 1745257812 (fix: typo in header)
[2] 2026-04-21T15:04:55Z — 1745252695 (feat: add login)
Select deployment to rollback to (number):On confirmation:
🔄 Rolling back to 1745257812...
✅ Rollback successful!
🌐 Your app is live at:
https://my-project.ghayma.appIf fewer than two successful deployments exist, rollback prints
❌ No previous deployments to rollback to.
ghayma delete
Delete a project and all its resources. Requires typing the project name to confirm before anything is sent to the server:
⚠️ This will permanently delete 'my-project' and all its deployments,
domains, and resources.
Type the project name to confirm:ghayma deleteOn confirmation the local .ghayma.json is removed as well, so you
can safely re-run init or link in the same directory afterwards.
This permanently deletes the project, all deployments, and every associated resource. Cannot be undone.
Deploying from the dashboard
You can also deploy without the CLI directly from dash.ghayma.cloud :
- Open your project → Deployments
- Drag & drop a
.zipor.tar.gzof your source code (excludenode_modules,.git,.next) - Optionally add a deploy message
- Click Deploy
Prepare your archive from the terminal:
# ZIP
zip -r my-project.zip . -x "node_modules/*" ".git/*" ".next/*" ".env.local"
# TAR.GZ
tar -czf my-project.tar.gz --exclude=node_modules --exclude=.git --exclude=.next --exclude=.env.local .Or link the app to a repository and let a push deploy it — see Deploy from GitHub.