Deploy with the API (CLI, CI & AI agents)
Deploy from the UI, from the CLI with an API token, or let an AI coding agent do it — with logs, deployment codes, and a full troubleshooting reference.
Two ways to deploy
- •Manual (UI): open your project, pick an environment, and click Deploy — see Getting started.
- •Headless (API token): authenticate with a personal access token and call the API from a script, CI, or an AI coding agent.
- •Either way, Shipyard builds from your connected repo (GitHub, GitLab or Bitbucket) — so push your code to the branch first, then trigger a deploy of that commit. With auto-deploy enabled on an environment, a push deploys automatically and you can skip the API call entirely.
1. Create an API token
- •In the console go to API tokens → New token, give it a name, and copy the value (shown once). It starts with shpat_.
- •A token acts as you, scoped to your workspace; it has no platform-admin access. Send it as a header: Authorization: Bearer shpat_…
- •Set your base URL + token as shell variables for the steps below:
- •export SHIPYARD_API=https://api.example.com
- •export SHIPYARD_TOKEN=shpat_xxxxxxxx
2. Find your workspace, project and environment
- •Your workspace (tenant) id: curl -s -H "Authorization: Bearer $SHIPYARD_TOKEN" $SHIPYARD_API/auth/me (use memberships[0].tenantId)
- •Your projects: curl -s -H "Authorization: Bearer $SHIPYARD_TOKEN" $SHIPYARD_API/tenants/<tenantId>/projects
- •A project’s environments: curl -s -H "Authorization: Bearer $SHIPYARD_TOKEN" $SHIPYARD_API/tenants/<tenantId>/projects/<projectId> (use environments[].id)
- •Match on id or slug, never on the display name — two projects in one workspace can share a name, and picking the wrong one is silent. To be certain you have the right environment, GET …/projects/<projectId>/environments/<environmentId>/runtime and compare its live URL with the one you are working on.
3. Trigger a deploy
- •POST to the environment’s deployments endpoint (optionally pass a branch + note):
- •curl -s -X POST -H "Authorization: Bearer $SHIPYARD_TOKEN" -H "Content-Type: application/json" -d '{"note":"deploy from CLI"}' $SHIPYARD_API/tenants/<tenantId>/projects/<projectId>/environments/<environmentId>/deployments
- •The response includes the deployment id. Poll its status: curl -s -H "Authorization: Bearer $SHIPYARD_TOKEN" $SHIPYARD_API/tenants/<tenantId>/deployments/<deploymentId>
- •Roll back or retry from the project’s Deployments tab, or via the rollback/retry endpoints.
Let an AI coding agent deploy for you
- •Create a token, then tell your AI coding agent: “Deploy this app to Shipyard — read https://example.com/docs/deploy and use SHIPYARD_API + SHIPYARD_TOKEN from my environment.”
- •The agent can push your code to your connected repo (GitHub, GitLab or Bitbucket), then run the calls above (resolve tenant → project → environment → POST a deployment) and report the live URL.
- •Keep the token in an environment variable or your secrets manager — never commit it. Revoke it anytime from the API tokens page.
Attach a managed database
- •Create a PostgreSQL or MongoDB under Databases. On creation (or later via “Use in app”) choose to inject its connection URI as a secret — into one project or the whole workspace.
- •Your app then reads DATABASE_URL (Postgres) or MONGODB_URI (Mongo) from the environment at build + runtime. Secrets stay scoped to your workspace only.
Identify a deployment by its code
- •Every deployment has a platform-wide unique reference like DEP-1042 — unique across the entire instance, not just your project. It is shown in the console (Deployments list + detail header) and returned by the API as deploymentRef (with the raw number as globalNumber).
- •A smaller per-project number (#7) still appears next to it. Quote the DEP- code when asking your AI agent or support to investigate a specific deploy — it identifies one deployment unambiguously at any scale.
Where to see logs (and copy errors)
- •Build logs — every deploy, live + historical, on the deployment detail page. API: GET $SHIPYARD_API/tenants/<tenantId>/deployments/<deploymentId>/logs (and an SSE stream at /stream).
- •Node runtime logs — a Node app’s container stdout/stderr, on the deployment detail page. API: GET $SHIPYARD_API/tenants/<tenantId>/releases/<releaseId>/runtime-logs (and /stream).
- •Static/React access log — static apps run NO process, so there is no stdout/stderr. Instead an Access log on the deployment detail page records requests the origin handled that failed (4xx/5xx) or were not a page load (e.g. a form POST). API: GET $SHIPYARD_API/tenants/<tenantId>/projects/<projectId>/environments/<environmentId>/access-logs (and /stream).
- •Every failure also sets the deployment’s error field (returned by GET $SHIPYARD_API/tenants/<tenantId>/deployments/<deploymentId>) and is folded verbatim into the build log — so any error can be copied straight out and pasted to an AI agent. Secret values are redacted automatically.
Static & React apps have no backend
- •A Static or React project is built once and served as files (a CDN-style origin). There is NO server running for it. So a form submit or a fetch to /api/… returns 404 Not Found — the response body reads “Cannot POST /_sites/…”. This is the most common “it worked on my machine” surprise.
- •Why it works locally: your dev server (Vite/CRA proxy) or a local API process answers that request during development — but that process is not part of a static build, so it does not exist in staging/production.
- •Fix (best, if your repo already has a server.js): set Runtime type = “Node server” (Project → Settings → Runtime configuration, or the “runs its own server” option at create time). Shipyard then builds the frontend and runs your Express/SSR server, which serves the built site AND /api on one origin — your relative /api/submit just works, no CORS. Your server must listen on process.env.PORT. See “React/Static apps that run their own server”.
- •Fix (if the API is a separate codebase): deploy your API as its own Node project and point the form’s submit URL at that app’s domain via a build-time variable (VITE_API_URL / REACT_APP_API_URL) under Secrets; enable CORS on the backend for your site’s domain. Or post to an external API / form-handling service. A purely static app cannot process its own submissions.
- •Diagnose it: open the failing deploy → Access log; the POST will appear as a 404. (A GET to an unknown route returns 200 because SPA routing falls back to index.html — only non-GET / API calls 404.)
Troubleshooting — build errors
- •“npm ci can only install with an existing package-lock.json” — the install command is npm ci but there is no lockfile at the build root directory. Fix: commit package-lock.json, or set the install command to npm install. (Shipyard auto-falls back to npm install when no lockfile is present, so also check your Root directory is correct.)
- •“Missing script: build” / “npm run build” fails immediately — the build command runs a script your package.json does not define. Fix: add the build script, or clear the build command. (Shipyard skips npm run <script> automatically when the script is absent.) Static sites with pre-committed files need no build command at all.
- •Build succeeds but the site is blank or 404s on every asset — the Output directory is wrong. Set it to the folder that contains index.html: Vite → dist, Create-React-App → build, plain static → public.
Troubleshooting — runtime & database errors (Node & Python)
- •“self-signed certificate” / DEPTH_ZERO_SELF_SIGNED_CERT — your app forces TLS verification on the database connection. An attached managed DB is reached over the private network and needs no TLS. Fix: remove the ssl option from your client/pool, or set ssl: { rejectUnauthorized: false }.
- •“App failed its health check” / “fetch failed” / ECONNREFUSED — the server did not listen on the assigned port, or the health-check path did not return 200 in time. Fix: bind to the port Shipyard injects as PORT (Node: process.env.PORT; Python: gunicorn app:app --bind 0.0.0.0:$PORT or uvicorn main:app --host 0.0.0.0 --port $PORT), and bind 0.0.0.0. Traffic only switches to a release after it passes the check.
- •Python: “ModuleNotFoundError” / gunicorn not found — your deps did not install into the venv. Shipyard installs into .venv inside the app (python -m venv .venv && .venv/bin/pip install -r requirements.txt) and puts .venv/bin on PATH at runtime; make sure requirements.txt is at your root directory and lists gunicorn/uvicorn.
- •“Missing DB_URI” / “DATABASE_URL is undefined” — your app reads a different env-var name than the secret key. Fix: attach the database under the exact key your code reads, or rename the secret in the project’s Secrets. Postgres defaults to DATABASE_URL, Mongo to MONGODB_URI.
Troubleshooting — quotas & API auth
- •403 “deployments … limit” — you reached the workspace’s daily deployment quota. Fix: wait for the window to reset, or ask a platform operator to raise it.
- •401 / 403 from the API — the token is missing, invalid, expired or revoked, or you called a platform-admin route. Tokens act as you and are workspace-scoped only. Fix: re-issue a token from API tokens, send it as Authorization: Bearer shpat_…, and only call tenant routes (/tenants/<tenantId>/…).
- •A URL is not available yet — a default URL is withheld until a deploy SUCCEEDS. If the env shows “becomes available after the first successful deploy”, the latest deploy has not gone live; check its build log/error.
Troubleshooting — the deploy is green but the app is broken
- •A successful deploy means the build produced a release and the container answered its health check. It says nothing about whether the app is correctly configured.
- •The app crashes on boot, or one route returns 500 carrying the app’s own validation error listing variable names (a Zod / pydantic / env-schema dump) — a required variable never reached the container. Almost always a secret sitting on the wrong scope, the wrong environment, or a same-named sibling project. See Configuration & secrets → “A secret on the wrong scope fails silently”, fix the scope, then redeploy.
- •Read the app’s own words first: GET $SHIPYARD_API/tenants/<t>/releases/<releaseId>/runtime-logs. The variable names it names are the exact keys to go looking for.
- •If the health check never passed at all, the release never took traffic — that is the port/ECONNREFUSED case above, not a configuration one.
Troubleshooting — platform-side failures (not your code)
- •“Infrastructure error” with NO build log, failing within seconds — the build never started, so your repository was never cloned and nothing was compiled. This is a fault on the platform side (for example the control plane could not reach your git provider), not a defect in your app.
- •Do not start editing code for it. Retry the deploy: POST $SHIPYARD_API/tenants/<t>/deployments/<deploymentId>/retry. If it fails the same way twice, quote the DEP- code to a platform operator — that alone is enough to locate the cause in the server logs.
- •Tell the two apart by the log: a genuine build failure always has build output ending at the failing command. A platform fault leaves an empty log and a generic error field.
Automated deploys: the loop an AI agent should run
- •Resolve ids first, and key off id/slug — never off a project’s display name, which is not unique. Confirm the environment id you picked serves the URL you mean (GET …/environments/<e>/runtime).
- •Push the commit to the branch → POST a deployment → poll GET $SHIPYARD_API/tenants/<tenantId>/deployments/<deploymentId> until status is a terminal value (succeeded / failed / cancelled).
- •On failed: read the error field AND the build log (and the runtime or access log for the app type). Map the message against the troubleshooting sections above, apply the fix, and redeploy. Reference the deploy by its DEP- code.
- •On failed with an EMPTY build log and “Infrastructure error”: the build never ran. Retry once instead of editing code — see “platform-side failures”.
- •On succeeded: do not stop at the status. Fetch the environment’s runtime (GET …/projects/<projectId>/environments/<environmentId>/runtime) for the live URL, then actually request that URL. A green deploy still returns 500 on every page when a required variable is missing.
- •If it does: read the runtime log, take the variable names the app complains about, and check each scope (environment → project → workspace) before assuming the code is wrong.
