# mars.cloud — container hosting for Mars IT School (AI-agent reference) > Deploy any Node / Python / Dockerfile app from a git repo (public anywhere, or any repo the user can read on Mars Git = git.marshub.uz) or a local directory > to `https://.marsdot.uz` with automatic HTTPS. Sibling of marshub.uz (static sites). > Human guide (RU/UZ/EN, screenshots): https://cloud.marsdot.uz/guide ## TL;DR for an agent ```bash npx -y marshub@latest login # once; the human confirms in Telegram (@marshubbot) npx -y marshub@latest cloud deploy --repo https://github.com/user/repo --name my-app --env KEY=VALUE npx -y marshub@latest cloud deploy --repo owner/repo --name my-app # owner/repo = Mars Git (git.marshub.uz), private OK # → {"name":"my-app","url":"https://my-app.marsdot.uz","state":"running","build":"node",...} curl -sI https://my-app.marsdot.uz | head -1 # expect HTTP/2 200 ``` Preconditions the agent must verify or tell the human about: 1. The app listens on `process.env.PORT` / `$PORT` (the platform sets `PORT=8080`). A hard-coded port is the #1 failure. 2. The repo is reachable over HTTPS: PUBLIC on github.com / gitlab.com / anywhere, OR on Mars Git (git.marshub.uz) where PRIVATE repos work as long as the user can read them (the server clones with its own token; Mars Git account = the user's Mars ID handle, else their Telegram ID). `GET /api/cloud/git/repos` lists what the user can pick. 3. The human is signed in with a Mars ID (the cloud is open to every Mars ID user). If the API still answers `403 cloud_locked`, stop and tell the human to report in the Telegram group `mars.space.devs` which account they signed in with (people sometimes have two Telegram accounts = two Mars IDs). Do not retry in a loop. 4. Secrets go into `--env` / the `env` field, never into the repo. 5. Needs Postgres/Redis? Create it first (`db create --type postgres|redis`), then deploy the app with `--env DATABASE_URL=` / `--env REDIS_URL=` from the create response. The DB hostnames resolve only inside mars.cloud — do not try to reach them from the agent's machine. ## Authentication - Same token as marshub: `~/.marshubrc` → `{"token":"mh_...","api":"https://marshub.uz"}` (90-day API key). - Obtain it with `npx -y marshub@latest login`: the CLI prints a `https://t.me/marshubbot?start=auth_...` link and polls; the human must press Start in Telegram. In JSON/agent mode the CLI emits `{"event":"auth_required","url":...}` — show the URL to the human and keep polling (CLI does it for you). - Check: `npx -y marshub@latest whoami --json`. - All HTTP calls: `Authorization: Bearer `. ## CLI (`npx -y marshub@latest cloud …`, v1.5+) Agent/JSON mode turns on automatically when `CLAUDECODE`, `CURSOR_AGENT` or `MARSHUB_AGENT` env is set, or with `--json`. Errors: `{"error":"","message":"..."}` + exit 1. | Command | What it does | |---|---| | `cloud deploy --repo [--branch B] [--name N] [--env K=V ...]` | Clone the repo on the server, build, run. Preferred path. `owner/repo` = Mars Git. | | `cloud deploy [dir] [--name N] [--env K=V ...]` | Pack a local directory (tar.gz, excludes node_modules/.git/.env) and build it. | | `cloud redeploy ` | Re-clone the stored repo/branch, rebuild, keep env. Use after the human pushes new code. | | `cloud ls` | List apps: name, url, state, repo, branch, commit, build. | | `cloud logs [--tail N]` | Container logs (stdout+stderr). First thing to read on `crash`/502. | | `cloud restart ` / `cloud rm ` | Restart / full teardown (container, image, route). | | `db create [--type postgres\|redis\|pocketbase]` / `db ls` / `db rm ` | Managed database. `postgres` (default in CLI: pocketbase) → `postgres://user:pass@postgres.marsdot.uz:5432/db`; `redis` → `redis://:pass@.db.marsdot.uz:6379/0`; `pocketbase` → `https://.db.marsdot.uz` (admin UI at `/_/`, superuser creds printed once). Postgres/Redis URLs are reachable ONLY from apps running on mars.cloud (private docker network, no public port) — pass them to the app via `--env DATABASE_URL=… --env REDIS_URL=…`. | Name rules: 3–30 chars, `[a-z0-9-]`, no leading/trailing/double hyphen, not reserved (`www api cloud admin app apps db fn mail id git staging test root marshub mars`). Omit `--name` to get a generated `adjective-noun`. ## MCP (Claude Code / Cursor / Codex) ```bash claude mcp add marshub -- npx -y marshub@latest mcp ``` Tools: `marshub_cloud_deploy` (repo | directory, name?, branch?, env?), `marshub_cloud_list`, `marshub_cloud_logs` (name, tail?), plus the static-site tools (`marshub_deploy`, …). ## REST API (base `https://cloud.marsdot.uz`) All under `/api/cloud`, Bearer auth, JSON unless noted. `403 cloud_locked` = the gate rejected the account. | Method | Path | Body / notes | Returns | |---|---|---|---| | GET | `/api/cloud/git/repos` | — | `{enabled, connected, login, repos:[{full_name,url,default_branch,private,description,updated_at}]}` — the user's Mars Git repos (own + collaborations + org). `connected:false` = no Mars Git account yet → tell the human to sign in at git.marshub.uz with Mars ID once. | | POST | `/api/cloud/deploy-git` | `{repo, branch?, name?, env?}` (`repo` = https URL or `owner/repo` on Mars Git; `env` = object of string→string) | `{name,url,port,state,version,build,dockerfile_generated,repo,branch,commit}` | | POST | `/api/cloud/deploy` | multipart: `file` = tar.gz build context, `name?`, `env?` (JSON string) | same | | POST | `/api/cloud/apps/:name/redeploy` | `{}` or `{env}` | same | | GET | `/api/cloud/apps` | — | `[{name,url,port,state,repo,branch,commit,build,created_at,updated_at}]` | | GET | `/api/cloud/apps/:name` | — | one app | | GET | `/api/cloud/apps/:name/logs?tail=200` | — | `text/plain` | | POST | `/api/cloud/apps/:name/restart` | — | `{name,restarted:true}` | | DELETE | `/api/cloud/apps/:name` | — | `{deleted:name}` | | GET/POST/DELETE | `/api/cloud/dbs`, `/api/cloud/db`, `/api/cloud/db/:name` | `{name, type?}` — `type` = `pocketbase` (default) \| `postgres` \| `redis` | `{name,kind,url,host,db_port,database?,user?,…}`; pocketbase adds `admin_url,admin_email,admin_password` (password only in the create response). `409 kind_mismatch` = name exists with another type. | | GET | `/api/cloud/volumes` | — | `[{name,size_gb,app,mount,used_bytes,created_at}]` — persistent disks (`app`/`mount` null = not attached; `used_bytes` null while unmounted) | | POST | `/api/cloud/volume` | `{name, size_gb? (1–5, default 1), app?, mount? (default "/data")}` | the volume; with `app` it is attached right away (that app's container is recreated) | | POST | `/api/cloud/volume/:name/attach` | `{app, mount?}` | the volume — container of `app` is recreated with the disk at `mount` (~2 s downtime) | | POST | `/api/cloud/volume/:name/detach` | `{}` | the volume — data kept, container recreated without it | | DELETE | `/api/cloud/volume/:name` | — | `{deleted}`; `409 attached` while attached — detach first | | GET | `/health` | public | `{"status":"ok"}` | A deploy call blocks until the container is running (typically 20–90 s, up to ~3 min on a cold Node/Python build). Use a ≥300 s HTTP timeout. ## Build contract (what the server does with the repo) 1. `git clone --depth 1 [--branch B]` of the HTTPS URL (Mars Git repos are cloned with the server's token after a read-permission check); `.git` is stripped; repo must be ≤ 500 MB; clone timeout 120 s. 2. If `Dockerfile` exists at the root → used as is. Otherwise a Dockerfile is generated from root markers, in this order: - `package.json` → `node:20-alpine`, `npm ci` (or `npm install`), runs `npm run build` if a `build` script exists (then prunes devDependencies), `CMD npm start` (or `node
`). No `start` script and no `main` → build error. - `requirements.txt` or `pyproject.toml` + entry file `main.py|app.py|server.py|run.py|manage.py` → `python:3.12-slim`, `pip install`. FastAPI/Starlette → `uvicorn :app --port $PORT`; Flask → `gunicorn :app`; `manage.py` → `migrate` + `runserver 0.0.0.0:$PORT`; Streamlit → `streamlit run`; else `python `. - `index.html` → static via `caddy file-server` (prefer marshub.uz for pure static). - Nothing matched → `400 no_dockerfile` — add one of the markers or a Dockerfile. 3. `docker build --pull`, then hardened `docker run`: 256 MB RAM (no swap), 0.5 CPU, pids 128, `--cap-drop ALL`, `no-new-privileges`, port published on loopback only; Caddy fronts `https://.marsdot.uz` with on-demand TLS. 4. After 1.5 s the container must be `running`, otherwise → `422 crash` with the last 50 log lines in `logs`. Generated Dockerfiles ignore `node_modules .git .env __pycache__ *.pyc .venv venv`. ## Error codes → what to do | `error` | Meaning | Agent action | |---|---|---| | `unauthorized` (401) | no/expired token | run `login`, have the human confirm in Telegram | | `cloud_locked` (403) | account rejected by the gate (should not happen) | tell the human to report in `mars.space.devs` which account they used; stop | | `bad_repo` / `clone_failed` (400/422) | not an https URL / `owner/repo`, repo missing, private on GitHub/GitLab, branch missing | fix URL, make repo public (or push it to Mars Git), check branch | | `repo_forbidden` (422) | private Mars Git repo the user cannot read | make it public or have the owner add the user as collaborator | | `repo_too_big` | > 500 MB | remove media/binaries or use tar deploy of a subdir | | `no_dockerfile` (400) | stack not detected | add `package.json` start script / `requirements.txt`+`main.py` / Dockerfile | | `build_failed` (422) | `docker build` failed; `message` has last 4000 chars of build log | read log; usually a dependency install failure | | `crash` (422) | container exited within 1.5 s; `logs` included | almost always wrong port → listen on `$PORT`; else missing env var | | `name_taken` (409) / `bad_name` (400) | name owned by someone else / invalid | choose another name | | `quota` (403) | > 10 apps / > 5 DBs / > 3 volumes or > 5 GB of volumes | delete unused apps / DBs / volumes | | `attached` (409) | deleting a volume still attached to an app | `detach` first, then delete | | `bad_mount` / `mount_taken` (400/409) | mount path invalid (`/data`-style absolute path, not a system dir) / already used by another volume on that app | choose another path | | `pool_full` (403) | the shared box has no room for more volumes | tell the human to ask in `mars.space.devs` | ## Limits 10 apps per user · 256 MB RAM / 0.5 CPU per app · repo ≤ 500 MB · tar upload ≤ 200 MB · ≤ 30 env vars, keys `[A-Z_][A-Z0-9_]*`, `PORT` is reserved · 5 databases per user (any mix of Pocketbase / PostgreSQL / Redis; Postgres = shared server, 20 connections per DB; Redis = 48 MB) · 3 persistent volumes per user, 1–5 GB each, 5 GB total · shared learning box, no SLA. ## Persistent volumes (disks) App containers are recreated on every deploy — files written inside the container are lost. For files that must survive (SQLite, uploads, caches) create a volume and attach it at a mount path; the app then reads/writes that directory as usual: ``` marshub volume create my-data --size 1 --app my-app --mount /data marshub volume ls / attach [--mount P] / detach / rm ``` Rules: the size is a hard cap (the app gets `ENOSPC` inside its disk, neighbours are unaffected) · one volume ↔ at most one app, an app may have several volumes at different paths · attach/detach recreates the container (~2 s) · deleting the app detaches its volumes and keeps the data; only `volume rm` deletes data · mount path must be absolute, not a system dir (`/proc /sys /dev /etc /usr /var …`). Point the app at the mount (e.g. `SQLITE_PATH=/data/app.db`); the directory is world-writable (mode 1777). For structured data a managed DB (`marshub db create`) is still the better fit. ## Links - Human guide: https://cloud.marsdot.uz/guide (`?lang=ru|uz|en`) - Dashboard: https://cloud.marsdot.uz/dashboard (Mars ID sign-in; deploy-from-git form; logs; redeploy) - Static hosting sibling: https://marshub.uz/llms.txt - Support: Telegram group `mars.space.devs`