Skip to Content
CLI ReferenceDeploy & Manage

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 init

You will be prompted for:

  1. Project name — becomes your slug and the default <slug>.ghayma.app domain
  2. 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, or static, or auto when nothing matches (see Supported frameworks).
  3. 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.
  4. 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.
  5. Site name — only when you deploy a site: leave empty for main (default), or type a name like frontend or admin
  6. 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 pro

The 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 pts

An 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" }
FieldDescription
project_idUnique project identifier
nameHuman-readable project name
slugURL-safe slug used in the default <slug>.ghayma.app domain
frameworkFramework tag written at init, auto-detected from your project files (auto when undetermined)
site_idID of the active site. Empty for a brand-new main site until the first deploy creates it
site_nameDisplay name of the active site
site_slugSlug of the active site
root_directoryMonorepo only: path from monorepo root to the app being deployed (e.g. apps/web)
uploadWorkspace manifest only: "app" uploads just root_directory, "workspace" uploads the whole workspace and builds root_directory inside it
install_commandOverride the detected install command
build_commandOverride the detected build command
start_commandOverride the detected start command
output_directoryStatic builds: directory your build emits into (e.g. dist)
portPort your app listens on (default 3000; 80 for static)
dockerfile_pathPath 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:

  1. .ghayma.json — the per-site keys above, or the site’s entry in a workspace manifest
  2. the settings saved on that site (Dashboard → the site → Settings)
  3. 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:

LabelDetected from
nextjspackage.json containing next
nodeany other package.json
gogo.mod
pythonrequirements.txt or pyproject.toml
phpcomposer.json
rubyGemfile
rustCargo.toml
staticindex.html and no package.json
autonothing 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 planUsed forNotes
Generated DockerfileNode projects — nextjs, nuxt, react, nodeNext.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 imagestatic, and any repo with no package.json that has an index.htmlGzip, caching headers, and SPA fallback to index.html. Set output_directory when your build emits to dist/build
Railpackpython, go, php, ruby, rust, java, deno, elixir, and any repo with no package.jsonRailpack 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.

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-project

After 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.json

One 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:

  1. a directory whose existing .ghayma.json already carries that site’s site_id
  2. a directory whose name equals the site’s slug or name
  3. 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.

cd my-monorepo ghayma link taarefni # or plain `ghayma link` and pick from the list

This 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 deploy

Pin the site in CI

ghayma deploy --site admin

A 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:

uploadThe CLI uploadsBuild runs in
"app"only root_directorythe root of the upload
"workspace"the whole workspaceroot_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|import and ghayma domain create resolve the site the same way deploy does: 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 create at a linked workspace offers to add the new site to the manifest, asking which directory holds it (you can skip).
  • ghayma site use is 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 deploy

Every 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 fromSite deployed
An app directory with its own .ghayma.jsonThe 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 itThat 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 admin

Nothing 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
  • --site naming a site other than the one the current directory pins → refused
  • --site naming 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 --site when the directory’s .ghayma.json named no site and deployed the project’s default site instead; update the CLI
  • a single-app .ghayma.json with an empty site_id on 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/ • **/*.log

See 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 No

A 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 connections

A No, a run without a terminal, or --no-connect connects nothing:

ghayma deploy --no-connect

The 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; --image deploys don’t.
  • Connecting needs write on the site. A project key never connects, and an account token needs the projects scope.
  • 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:

FlagDescription
--fromSite to promote from, by name or slug (required)
--deploymentDeployment of that site to promote (default: its current live one)
--siteSite to promote into (default: the project’s default site)
--messageMessage 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, dist

For 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.local

If 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
FlagWhat 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)
--deployDeploy the image once it is pushed
--prodWith --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 --prod

Which 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 500

ghayma 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.app

If 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 delete

On 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 :

  1. Open your project → Deployments
  2. Drag & drop a .zip or .tar.gz of your source code (exclude node_modules, .git, .next)
  3. Optionally add a deploy message
  4. 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.