--- name: zerops-deploy description: Deploy a local application to Zerops, verify its public URL, and redeploy changes. Use when asked to deploy, publish, or update an app on Zerops, including first-time project and service creation. --- # Deploy an app to Zerops Use official `zcli` from the user's local checkout or cloud sandbox over HTTPS; no VPN, SSH, or Zerops container is needed. Adapt deployment configuration; keep the app. ## 0. Establish prerequisites Use `zerops-setup` to install **zcli v1.1.3 or newer**, sign in with browser login, and select the organization and region. Run `zcli version` to verify the installed binary. Do not assume npm supplies browser login: at the v1.1.3 audit, npm lagged; use the setup skill's verified GitHub release route. In a cloud sandbox, show the browser login URL to the user. For CI/headless execution, use only an already provisioned `ZEROPS_TOKEN` environment variable; never request a token in chat. Never read CLI credential files, print tokens, or put secrets in tracked YAML. Use explicit IDs and `--setup`, even if a scope was saved. Fill example variables with discovered non-secret values; do not execute them empty. ## 1. Inspect the application - Read manifests, lockfiles, server entrypoints and relevant build configuration. Determine the runtime/version, framework, package manager, actual build output, foreground start command, listener port and a safe HTTP verification route. - Find environment-variable **names and usage in code**, without dumping secret files. Identify required database, cache, queue and storage dependencies. Keep existing remote dependencies when intended; use managed services when needed. - Identify the build root and monorepo workspace dependencies. Choose one runtime service per app unless distinct deployable processes clearly require more. Put persistent application data in suitable storage, not runtime local files. - Match the configured port and environment variables to the existing app. Change its bind address only for explicit loopback (`localhost`, `127.0.0.1`, `::1`) or a verified loopback-only framework default; otherwise leave it alone. Node/Express `app.listen(PORT)` already listens on all interfaces. See `zerops-yaml` for framework defaults. `run.ports` does not set the app's listener or make it read `PORT`. For a matching starting point, read only the relevant example: [Node/Express + PostgreSQL](references/node-express-postgres.md), [Python/FastAPI](references/python-fastapi.md), or [static site](references/static-site.md). Adapt its assumptions to the real app. ## 2. Prepare zerops.yaml Use `zerops-yaml` to create or correct the selected setup in `zerops.yaml` (or an existing `zerops.yml`). Make `--setup` match that entry explicitly. Keep build and runtime bases compatible with the app and any native dependencies. Check these five common traps before pushing: 1. **Artifact paths:** `deployFiles: dist` retains `dist/` under `/var/www`; `dist/~` puts its contents at the runtime root. Match the start command and document root to the resulting paths. 2. **Missing dependencies:** build and runtime are separate containers. Ship production dependencies such as Node `node_modules` or Python vendored packages unless the output is self-contained; build-only global installs do not carry over. 3. **Wrong lifecycle phase:** `run.prepareCommands` runs before deployed files arrive. Supply preparation inputs with `build.addToRunPrepare`; put operations needing deployed code or database connectivity in appropriate runtime initialization. Initialization runs on every container start: make it idempotent and coordinate migrations when multiple containers can run them. 4. **Unreachable server:** keep `run.start` in the foreground, check the listener using step 1, match its port to `run.ports`, and set `httpSupport: true`. Static/webserver runtimes can supply their own server; follow their recipe. 5. **Missing runtime configuration:** build variables do not become runtime variables. Wire app keys explicitly under `run.envVariables`, for example `DATABASE_URL: ${db_connectionString}` for service `db`. Do not self-shadow references or put database secrets into browser bundles. Configure `deploy.readinessCheck` against an existing safe endpoint when useful; `run.readinessCheck` is not the supported location. Use `zerops-yaml` for the exact check schema. A successful build alone does not prove a new version activated. ## 3. Resolve or create the target Reuse a matching project/service before creating infrastructure: ```sh zcli project list zcli service list --project-id "$PROJECT_ID" ``` These return tables, not JSON. Match the intended organization/project, service name, type and purpose; resolve ambiguous names before a write. Record the actual project and service IDs. Confirm the service ID occurs in the selected project's list: a service ID takes precedence over the project flag, so passing both is not a consistency check. For a first deployment, use `zerops-import` to prepare `import.yaml` with the project, app runtime and needed managed services, including explicit environment references. Select a suitable core and service profiles for the workload; creating services incurs resource charges. Import non-interactively: ```sh zcli project project-import ./import.yaml --org-id "$ORG_ID" ``` For missing services in an existing project, prepare a services-only import using `zerops-import`, then run: ```sh zcli project service-import ./services.yaml --project-id "$PROJECT_ID" ``` Require import exit 0: it waits for the returned provisioning processes. Relist to capture IDs and inspect service status. Check dependencies finished provisioning before pushing; an app without code is not yet an HTTP health failure. If an import fails or is interrupted, relist and inspect before retrying to avoid duplicates. Set `enableSubdomainAccess: true` in the HTTP app's import; after push, follow [Activate and verify public access](#activate-and-verify-public-access). ## 4. Push and observe the deployment Run push/import commands without a pipe. Never append `2>&1 | tail -15`: it hides the CLI exit status. For compact logs, redirect to a file, save `$?` immediately, then read the log; for a Bash pipeline, first enable `set -o pipefail`. Stop on a nonzero CLI exit; a working URL cannot turn a failed push into success. Choose the source snapshot deliberately. For a normal Git checkout with a real `.git` directory at the chosen root and at least one commit, include the local changes being deployed with: ```sh zcli push --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" \ --setup "$SETUP" --working-dir . --workspace-state all ``` `all` is the default: it includes staged, unstaged and nonignored untracked changes, not just HEAD. Use `--workspace-state clean` only to deploy committed HEAD, or `staged` for the index. Do not silently commit the user's work. Git mode does not use `.deployignore`; tracked secret files remain eligible even if later ignored. Review the file selection without opening credential files. For no Git, a checkout with no commits, a linked worktree, or a self-contained subdirectory, prepare `.deployignore` at the upload root, then use: ```sh zcli push --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" \ --setup "$SETUP" --working-dir "$APP_ROOT" --no-git ``` Exclude `.git`, actual secret files, local caches and unwanted dependencies from this source upload. `.deployignore` uses Git-ignore patterns but is not loaded from `.gitignore`; there is no automatic `.git` exclusion. Do not blindly copy ignore rules that remove inputs or artifacts the deployment needs. Do not combine `--no-git` with `--workspace-state` or `--deploy-git-folder`. For a monorepo needing root lockfiles/shared packages, keep the upload root at the repository root and adapt build paths through `zerops-yaml`. Do not assume Git `all` works from arbitrary subdirectories: its temporary index lives under `.git` in the working directory. `--zerops-yaml-path path/to/zerops.yaml` selects a config relative to `--working-dir` (or accepts an absolute path); it does not include otherwise excluded app source. Default lookup tries `zerops.yaml`, then `zerops.yml`. Keep the push running until it exits; omit `--no-wait` and `--disable-logs`. TTY push displays build/runtime preparation logs, but non-TTY agents may not see those lines. Once the current build exists, stream build logs explicitly in a separately managed process while awaiting the push: ```sh zcli service log --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" \ --show-build-logs --follow --format jsonstream ``` This follows the **latest** app version, so avoid overlapping pushes to the same service and correlate logs to this attempt. During upload it can report that no build is available; wait for this build rather than reading an earlier version as proof. Stop the follower when the push finishes. If concurrent processes are unavailable, await push and retrieve a finite build log using the next section. Record the push process's own exit code, not a log follower's or a pipeline's last command. A normal completed waiting push returns 0 on success and 1 on failure. Cancellation can return 0; an interrupted/timed-out command is unverified, and `--no-wait` exit 0 would prove only submission. The CLI waiter has no overall deployment deadline. Inspect state before resubmitting an interrupted push. ## Activate and verify public access Immediately after the **first successful push**, always run this for the HTTP service, even when its import had `enableSubdomainAccess: true`. The import flag records intent; it does not guarantee a live route. Activation is idempotent and safe on redeploys: ```sh zcli service enable-subdomain --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" ``` Require activation exit 0 before probing. Deployment success requires **a completed push with exit 0 plus the URL status/content check below**; neither logs nor a URL alone suffices. Retrieve only the public subdomain value, without revealing other environment data: ```sh PUBLIC_URL=$(zcli project env --project-id "$PROJECT_ID" --service "$SERVICE_NAME" \ --template '{{if eq .Key "zeropsSubdomain"}}{{.Value}}{{end}}' | tr -d '"' | grep -m1 '^https://') ``` The value comes back wrapped in double quotes; the `tr` strips them, or curl rejects the URL. `--service` here takes the service **name**. Do not add `--reveal`, print the whole environment, or evaluate the output as shell code. The platform supplies this variable with subdomain access, but its availability through this CLI read is not guaranteed. Ignore blank lines and inspect a returned value as data; it can be shell-quoted. If absent, use the service's **Subdomain & domain & IP access** view in the Zerops GUI to copy the assigned HTTPS URL. Multiple HTTP ports can have different URLs; select the app's port. Never derive a hostname from IDs/names. If neither route is accessible, report URL verification as pending and request the assigned public URL, not credentials. Set `PUBLIC_URL` to that assigned address and `CHECK_URL` to its safe verification route (`"$PUBLIC_URL/"`, or the app's health path). Poll with a fixed bound: ```sh http_ready=false for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do if curl --fail-with-body --silent --show-error --location \ --connect-timeout 5 --max-time 10 "$CHECK_URL"; then http_ready=true break fi [ "$attempt" -eq 12 ] || sleep 5 done [ "$http_ready" = true ] ``` This permits at most 12 requests and 11 waits (under three minutes of probe time). On exhaustion, stop and diagnose with `zerops-operate`; do not report success. These bounds are workflow choices. Check expected status **and content**, including the new version's marker or changed behavior on redeploy; an old version can still serve after failed activation. For static sites, check an emitted asset too: SPA fallback can return HTML with 200 for a missing file. ## Diagnose and retry with a bound Retrieve finite logs for the current attempt; sanitize secrets before quoting logs: ```sh zcli service log --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" \ --show-build-logs --limit 100 --format json zcli service log --project-id "$PROJECT_ID" --service-id "$SERVICE_ID" \ --limit 100 --format json ``` Use `zerops-operate` for deeper diagnosis. Map the observed failure to its owner: | Evidence | Fix/check | | --- | --- | | Setup not found or wrong source tree | Correct `--setup`, upload root and config path. | | Build command fails | Correct actual package manager, lockfile, runtime or build inputs. | | Runtime module/file missing | Correct `deployFiles`, production dependencies and start path. | | Initialization fails or old version still serves | Inspect initialization/activation; fix lifecycle placement and migration behavior. | | 502 or connection refused | Follow [public access activation](#activate-and-verify-public-access), then check crash logs, listener (step 1), foreground start, port and HTTP routing. | | Database/config error | Check explicit env references, dependency readiness and private connection settings without exposing secrets. | | 401/403 from the CLI/API | Use `zerops-setup` for session/organization permissions; do not blindly retry writes. | As a workflow bound, allow the initial push plus at most two evidence-based repair pushes. Stop earlier if the same failure recurs without a new diagnosis, a required credential/permission is unavailable, or the fix would expand the user's requested scope. Report the failing stage, sanitized evidence, changes attempted and exact next action. Do not delete/recreate services or reset databases as a retry tactic. ## Remember the target and redeploy Recommend a small repo note such as `DEPLOYMENT.md`, following existing repository documentation conventions. Record only organization/region, project/service names and IDs, setup, upload root, Git/no-Git choice, exact push command and public URL. These identifiers are not credentials; never include tokens or secret values. Prefer this note over global `zcli scope project`: global scope remembers only the project and can affect another checkout. Keep explicit IDs on every service command. For a redeploy, read the note, confirm the target, update `zerops.yaml` only as needed, repeat the same push and [public access verification](#activate-and-verify-public-access). Import only when infrastructure actually needs adding; do not recreate the project on each push. ## Report the outcome Return the verified URL and HTTP/content result, project/service names, and exact redeploy command. State any pending verification instead of claiming the app is live. Summarize infrastructure created and give the documented cost caveat: project core plus service resources and applicable extras are billed; a free Lightweight core does **not** make its services free. Link current [pricing](https://docs.zerops.io/company/pricing) instead of inventing an estimate. For production traffic, explain that the generated subdomain is intended for development/testing and route custom-domain work through `zerops-operate`. ## Sources Command behavior was checked against [zcli v1.1.3](https://github.com/zeropsio/zcli/tree/v1.1.3): `src/cmd/{servicePush,serviceLog,serviceEnableSubdomain,projectImport,projectServiceImport,projectEnv}.go`, `src/archiveClient/handler_gitArchiver.go`, `src/yamlReader/zeropsYaml.go`, `src/uxBlock/spinner.go`, and `src/uxHelpers/spinner.go`. Recheck newer behavior with the installed command's `--help`; do not invent flags such as list `--json`. Platform references: [YAML specification](https://docs.zerops.io/zerops-yaml/specification), [deployment lifecycle](https://docs.zerops.io/guides/deployment-lifecycle), [public access](https://docs.zerops.io/references/networking/public-access), and [public OpenAPI](https://api.app-prg1.zerops.io/api/rest/public/swagger/openapi.yml). Routing sequence: ZCP `internal/knowledge/{themes/core,guides/public-access}.md`. Listener: [Node.js listen](https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback).