Deploy contract

What CloudGrid does to your code on the way to a running service, and what you can change about it.

The platform owns the build recipe. You write source code and cloudgrid.yaml; the platform turns that into a container image, a Kubernetes Deployment, a Service, an Ingress, probes, and env injection. This page lists what the platform decides, what you control, and what is not user-overridable today.

If you came here from a build failure asking "where do I change X" — start with the override table and the versioning policy. If X is not in the override table, it is not user-changeable today; the path is to ask for it, not to work around it.

1. What the platform decides for you

These are platform choices. They apply to every service of the given type. No yaml field overrides them today.

Container port

Every service container listens on 8080 by default, regardless of type. The per-service port: yaml override swaps the listener for individual services whose framework hard-codes a different port — the default below is what applies when port: is absent.

Base image per service type

The platform's build pipeline synthesizes a Dockerfile from the service's type:. You do not supply one. The base images today:

type:Base image (runtime)Builder image (when applicable)
nodenode:22-alpine
node + lang: typescriptnode:22-alpinenode:22-alpine (stage 1: tsc)
nextjsnode:22-alpinenode:22-alpine (stage 1: next build)
pythonpython:3.12-slim
staticnginx:1.28-alpinenode:22-alpine (when build: is declared)
cron (run: job)inherits from detected language (node or python)
cron (run: http(s)://…)no image — platform requests the URL on schedule

Static-build runner

For type: static with a build: block, the platform runs your command (e.g. npm run build) in a build step using the runner image node:22-alpine (Node 22 LTS). This is the floor for Astro 6, Next.js 16, and Vite 7.

Synthesized nginx config (static services)

The platform writes the nginx config for type: static at image-build time. You do not supply nginx.conf. The synthesized config:

If a nginx.conf is present in your service folder, it is ignored. The platform does not expose nginx tunables (gzip, cache headers, rate limits) through yaml today.

User Dockerfile — always ignored

Any Dockerfile in your project is silently stripped before the upload reaches the build, regardless of whether one exists at the project root, in services/<svc>/, or anywhere else.

You do not need to fix a failing build by editing a Dockerfile. The fix is one of:

Env injection (auto-injected, in this order)

Every service container receives env vars in this order. Later sources override earlier ones, but the platform protects reserved keys against override.

  1. Reserved platform vars. PORT, APP_NAME, SERVICE_NAME, NODE_ENV, CLOUDGRID_ORG_ID, CLOUDGRID_ENTITY_ID, RUNTIME_GATEWAY_URL.
  2. Resource URLs for whatever the entity declares in requires:MONGODB_URL, REDIS_URL, N8N_WEBHOOK_URL, plus the GCP service account binding when gcp: is declared.
  3. Sibling-service URLs — one <UPPER_SVC>_URL per other service in the same yaml (e.g. a web service that has a sibling api gets API_URL).
  4. Vault entries declared in the top-level vault: block — name on the left of each entry, resolved value on the right.
  5. Static env from yaml — anything under services.<svc>.env:.
  6. Runtime env — anything set via grid env set.
  7. Secrets — anything set via grid secrets set (mounted from a per-entity Kubernetes Secret).

The platform's reserved keys cannot be overridden by the last three sources. grid env set PORT=… fails with RESERVED_KEY.

Probes

By default the platform configures Kubernetes startup, liveness, and readiness probes as TCP probes on the container port. A successful TCP connect counts as ready. You do not need a /health route.

You can opt in to a stricter HTTP probe per service with health: in yaml:

health: valueProbe shape
absent / falseTCP probe on the container port (default)
trueHTTP GET on /health
'/api/health'HTTP GET on that path

A separate external rollout probe (run after rollout) hits / and treats any HTTP response below 500 as up — so a 404 from a Next.js root still passes the rollout gate.

Image policy

You do not manage image tags or build numbers.

2. What you control via cloudgrid.yaml

The field-by-field reference is the CLOUDGRID.md file the CLI installs into each new project. Short summary of what is under your control today:

Anything not listed here — base image, Node version for server types, nginx config, build command flags for synthesized Dockerfiles — is platform-managed today.

3. What you can override and what you can't

KnobUser-controllable?HowNotes
Container portYesservices.<svc>.port: <1-65535> (any non-cron type)Default 8080. Sets containerPort, all three probes, Service.targetPort, and the auto-injected PORT env var. Service.port (the cluster-facing port reached through the Ingress) stays at 80 regardless.
Service base imageNoPlatform-chosen per type:.
Node version (node, nextjs)NoNode 22 LTS today. The node_version: knob (below) is static-only for now.
Node version (static-build runner)Yesservices.<svc>.node_version: '18' | '20' | '22' | '24' (static type only)Default '22' (Node 22 LTS). Falls through to the platform default when absent. Quote the value — bare yaml integers are rejected.
Python versionNoPython 3.12 today.
nginx config (static services)NoSynthesized at build time; nginx.conf in your repo is ignored. Tunables (gzip, cache headers) not exposed.
DockerfileNo (always ignored)A Dockerfile in your upload is stripped.
Probe shapeYesservices.<svc>.health: (absent / true / '/path')Default TCP; opt in to HTTP.
Build command (static)Yesservices.<svc>.build.command:Required for frameworks that need a build step.
Build output dir (static)Yesservices.<svc>.build.output: (default dist)Where the platform expects index.html after build.
Build-time env (static)Yesservices.<svc>.build.env: (string-to-string map)Build-time only — values are exported into the static-build step before command runs and are baked into the bundle (Vite VITE_*, Next.js export NEXT_PUBLIC_*, build-time feature flags). They do not reach the running container. Reserved keys: PORT, APP_NAME, SERVICE_NAME, NODE_ENV, and the CLOUDGRID_* namespace. The _URL suffix is not reserved here (that reservation is runtime-only; VITE_API_URL and friends are the canonical use).
Env injected into the podYesservices.<svc>.env:, grid env, grid secrets, vault:Reserved keys (PORT, APP_NAME, …) cannot be overridden. Runtime only — for build-time env see the row above.
Sibling-service URLsNo (auto)One <UPPER_SVC>_URL per sibling.
Resource URLs (Mongo, Redis, …)No (auto)Injected when declared in requires:.
Image tag in DeploymentNoAlways :latest.

If a knob you need is in the "No" column and you have a real use case, ask for it — don't work around the platform.

4. Smart defaults from package.json

At plug time the platform reads each uploaded service's package.json and fills yaml fields you left blank. The mechanism is additive — an explicit yaml field is never overridden; a smart default only applies when the yaml side is silent.

Precedence

Highest to lowest:

  1. Explicit yaml field — whatever appears in cloudgrid.yaml.
  2. Smart default — detected from package.json when the yaml is silent.
  3. Platform default — the values above (Node 22 LTS, port 8080, no build command).

Any yaml that worked before this layer existed continues to deploy identically — smart defaults can only fill blanks.

Detection table

FieldDetection ruleYaml slot filled when silent
frameworkdependencies.next → next; .astro → astro; .remix → remix; @sveltejs/kit → sveltekit; @vitejs/plugin-react → vite-react; @vitejs/plugin-vue → vite-vue; vite (bare) → vite; .express → express; otherwise → static(no yaml slot today — logged only)
node_versionengines.node semver range → highest supported major in {18, 20, 22, 24} that satisfies the rangeservices.<svc>.node_version (static type only)
portscripts.start parsed for --port=NNNN, --port NNNN, -p NNNN, or PORT=NNNN node …services.<svc>.port (non-cron types)
build_commandscripts.build verbatimservices.<svc>.build.command (static type only)
start_commandscripts.start verbatim(no yaml slot today — logged only)

The semver-range resolver picks the highest matching supported major. engines.node: ">=20" resolves to 24; ^20 resolves to 20; >=18 <22 resolves to 20. A range that excludes every supported major (e.g. <18) resolves to nothing and the platform default applies.

What happens at plug time

For every service whose upload includes a parseable package.json, the deploy log surfaces one line:

Detected next from package.json. Using node_version 24, build_command 'next build'.

A service whose only signal is the static fallback (no framework dependency detected, no other field changed) produces no line — there is nothing to learn from a no-op detection.

Edge cases

5. Versioning policy

When the platform changes a base image or a runner version, it is a fleet-wide change — every service of the affected type rebuilds on its next grid plug and runs against the new floor.

What counts as a version change

Patch-level bumps within the same minor (e.g. node:22-alpinenode:22.10-alpine as the upstream image rolls forward) are not announced — the alpine tag is a moving floor and re-pulls track upstream.

Announcement mechanism

Every version change ships via a CloudGrid release. The release note names the change, the floor it lifts, and any known-incompatible frameworks.

Why bumps happen

The platform avoids bumps that don't gain anything. Bumps are intentional, named, and tied to a rollout date.

Backwards compatibility

The platform tries to keep bumps backwards-compatible: a service that worked on the previous floor should keep working on the new floor. When a bump cannot be backwards-compatible — for example, dropping a Python minor — the release note calls it out and the platform offers a version lever (like node_version:) for that cycle.