Browse sections

Container Applications: Deploy, Promote & Roll Back Long-Lived Services

Run any OCI image as a long-lived service: build it from a Dockerfile or pull it from a registry, health-check it, give it a preview URL, then promote it to production with instant rollback. HTTP services get a hostname; databases and other TCP services get a dialable endpoint.

A function is code the platform runs inside its own runtime image, one invocation at a time. An application is an arbitrary container the platform keeps running: no idle timeout, raw reverse proxy (WebSockets and SSE pass through), and HTTP and raw TCP — Postgres, Redis, SMTP, SSH. Manage applications on the Apps page, with inquir apps in the CLI, or through the REST API; the vocabulary is the same everywhere.

Applications, releases, previews

  • Application — the service definition: name, ports, health check, resources, env vars, outbound policy. Editing it changes the next release, never a running one.
  • Release — an immutable image digest plus a frozen copy of the runtime spec and env, running as one container. Its lifecycle is Queued → Starting → Health check → Healthy → Serving → Rollback reserve → Stopped, or Failed. A release that fails its health check never receives traffic.
  • Preview — every new release comes up on its own preview URL ({app}-{release}.preview.apps.…) so you can test it before anyone else sees it. Previews are reaped after 24 hours unless promoted.
  • Promote — points production traffic (the production hostname and every custom domain) at a release. Manual, and restricted to owners and admins.
  • Roll back — points production at the previous release again. The previous container is kept running for the drain window, so rollback is instant while that reserve lasts.

Deploy your first service

With the CLI installed and logged in, an HTTP service is three commands. inquir apps deploy with no flags packages the current directory (or --dir), builds it from its Dockerfile on the platform, and releases the resulting image; pass --image to release a prebuilt image instead. The command waits until the release is healthy and prints its preview URL.

quickstart
# 1. Create the application — an HTTP service listening on port 3000
inquir apps create web --port 3000

# 2. Deploy: builds the Dockerfile in the current directory, releases the image
#    and waits until the health check passes. The release gets a preview URL.
inquir apps deploy web

#    …or release a prebuilt image instead of building
inquir apps deploy web --image nginx:alpine

# 3. Promote: production traffic moves to this release (owner/admin login)
inquir apps promote <releaseId>

inquir apps status web            # endpoints, hostnames, recent releases
inquir apps logs web --tail 100   # stream container logs
inquir apps rollback web          # back to the previous production release

The dashboard offers the same flow: New application, then New release (image or source build) on the Releases tab, then Promote on the release row. Ids and names are interchangeable in the CLI, and --json is accepted everywhere.

Health checks

A new release is gated behind a startup health check (up to 120 s by default; the container is inspected every 2 s so a crash fails fast); services with an HTTP port are re-probed periodically while they run (intervalSeconds, default 30 s). Three probe types exist; the default is http when the application has an HTTP port and tcp otherwise.

ProbeWhat it proves
httpGET on the health path (default /) answers with a status below 500 — a server that 404s its root is listening; only connection failures and 5xx count as unhealthy.
tcpThe port accepts a connection and keeps it open briefly.
commandA command run inside the container exits 0.

Use command for databases. Docker accepts connections on a published port before the container is actually listening, so a tcp probe can pass while Postgres is still initialising. --health-command "pg_isready -h 127.0.0.1" (or redis-cli ping) asks the service itself. In the API this is runtime.healthcheck: { type, path | command, port?, intervalSeconds, timeoutSeconds }.

Preview URL vs production URL

An application with an HTTP port has one production hostname ({app}.apps.…, plus any custom domains) that always follows the promoted release, and each release has its own preview URL until it is promoted or stopped. TLS is terminated at the edge for both. A pure-TCP service has no URL at all — it has endpoints (host:port per public TCP port) shown on the Releases tab and by inquir apps status.

Promote, roll back, stop

Promotion is a deliberate, confirmed step — the UI and CLI tell you what will happen before it does:

  • If the application has production-only env vars, the container is recreated under the production profile (health-gated) before traffic moves, so a preview never carried production credentials.
  • The release's preview URL retires once it serves production; the production hostname and every verified custom domain start routing to it.
  • The previous production release keeps running for the drain window (5 min by default, drainGraceSeconds per release) as the instant-rollback reserve. In-flight requests and WebSockets on it finish on their existing sockets.

Roll back (inquir apps rollback <app>, POST /v1/apps/:id/rollback) flips production back to the previous release: instant (200) while its container is still in the reserve, otherwise it is restarted from its own frozen image first (202). Rolling back is itself a promotion, so the same drain window applies to the release you left.

Stop (inquir apps stop <release>, DELETE /v1/releases/:id) stops any release that is not serving production — previews and superseded releases alike; the routed release answers 409. Stopping is how you free a pinned host port or a slot in the live-release quota. Archive (inquir apps delete) stops every release and unbinds every hostname; the name is free again afterwards.

TCP ports

Ports other than the primary HTTP port are declared with --tcp name:port[:public][:hostPort] (or runtime.ports[] with protocol: tcp). A TCP port is published on a host port and dialled directly — no runtime adapter, no HTTP assumption — so a database or SSH server is a first-class service:

postgres
# A database: no HTTP port, one public TCP port pinned to host port 25432,
# health-checked by asking the service itself (not by a TCP connect)
inquir apps create db \
  --tcp postgres:5432:public:25432 \
  --health-command "pg_isready -h 127.0.0.1" \
  --set POSTGRES_PASSWORD=secret
inquir apps deploy db --image postgres:16-alpine

# inquir apps status db  ->  TCP · tcp.inquir.org:25432 (postgres)
psql "postgres://postgres:secret@tcp.inquir.org:25432/postgres"
  • public publishes the port beyond loopback and gives the release an endpoint such as tcp.inquir.org:25432. Without it the port binds to loopback on the host (useful for debugging only). A public TCP port is reachable by anyone who can reach the host — the platform terminates no TLS and checks no credentials on raw TCP, so the service must authenticate its own clients.
  • hostPort pins the published port so connection strings survive a restart; an unpinned port is reassigned every time the container is recreated. Pins must fall inside the platform's port range (20000–39999 by default; the create form shows the live range) and are a host-wide reservation: a second live release pinning the same port is refused with 409 until the holder is stopped.
  • HTTP ports are never published separately — they are served over the hostname (TLS, Host routing, release lifecycle), so public and hostPort are rejected on an HTTP port. An application may have no HTTP port at all.

Custom domains

Besides its generated hostname, an application is reachable at any hostname you bind to it — from the Domains tab or the CLI. A new hostname starts unverified: it is not routed and no certificate is issued until you prove ownership with a TXT record _inquir-verify.<host> carrying the printed token, plus a CNAME to domains.inquir.org (or an A/ALIAS to the ingress). Then verify:

domain
# Bind the hostname — it starts unverified and prints the DNS records to publish
inquir apps domain shop shop.example.com
#   TXT   _inquir-verify.shop.example.com  ->  <token>
#   CNAME shop.example.com                 ->  domains.inquir.org

# Once DNS has propagated, prove ownership: the hostname becomes routable and gets TLS
inquir apps domain shop shop.example.com --verify

inquir apps domain shop                            # list bound hostnames
inquir apps domain shop shop.example.com --remove  # unbind

Custom domains are always production hostnames and follow the promoted release from the moment they verify; you can add one before or after the first promote. Platform-owned names (the apps/preview suffixes, the marketing and dashboard hosts, hostnames already claimed by a gateway custom domain) are refused. Domain changes move production traffic, so they need an owner or admin.

Environment variables

Set env vars on the application (--set KEY=VALUE, the Variables section in Settings, or envVars in the API). Values are encrypted at rest, masked on every read, and frozen into each release when it is created — changing a variable affects the next release, not a running one. Redeploy ("Deploy again") to pick up new values.

Keys listed in productionOnlyEnvKeys are withheld from a release while it runs under the preview profile and injected only when it is promoted (the container is recreated under the production profile first). Use it for production credentials, so a preview URL handed to a teammate or an agent cannot carry them. PORT is injected automatically from the primary HTTP port.

Outbound network

--egress none|full (networkPolicy.egress) sets whether the container may open outbound connections at all. none puts it on an isolated network with no route out — right for a database that should only be dialled, wrong for anything that calls an API. Per-host allowlists are available for functions but not yet for applications; the API answers 400 for allowlist here rather than accepting a policy the release could not honour.

Quotas and defaults

Every workspace has a ceiling on live releases and on resources per release; the create form and GET /v1/apps/limits report the values in force. Platform defaults:

LimitDefault
Live releases per workspace (previews + production + reserve)10
Memory per release512 MB (ceiling 2,048 MB)
CPU per release0.5 vCPU (ceiling 2 vCPU)
Startup health-check deadline120 s
Drain window (rollback reserve)5 min
Preview release lifetime24 h
Restarts before a release is marked failed (crash loop)5

Roles and API keys

Any workspace member — including the developer role — can create applications, deploy releases, watch logs, stop non-production releases and edit settings. Promote, roll back, custom domains, archive and exec need an owner or admin who is logged in (a browser session or a personal access token minted by inquir login). Actions the current role cannot perform are shown disabled with the reason.

Workspace API keys cannot promote, however permissive their role: promotion and exec require a user identity. That is deliberate — a CI or agent key with the deploy scope can build and ship previews and read their status and logs, but cannot move production traffic or read the secrets a live container holds.

REST API

Everything the CLI and dashboard do goes through /v1/apps…. Long operations answer 202 and expose a status URL: a release row carries the bare image ref until the digest is pinned, then imageDigest flips to sha256:….

EndpointPurpose
POST /v1/apps, GET /v1/appsCreate an application (env values are masked on every read) / list, cursor-paginated.
GET /v1/apps/:id, PATCH /v1/apps/:id, DELETE /v1/apps/:idRead (production pointer, URLs, previews, domains, recent releases) / update — masked env values are merged, not overwritten / archive.
POST /v1/apps/:id/releasesCreate a release from {buildId} or {imageRef, runtime?} → 202; the pull and health gate run in the background.
GET /v1/releases/:id, GET /v1/releases/:id/logsRelease status projection / SSE container logs.
POST /v1/releases/:id/promote, POST /v1/apps/:id/rollbackMove production traffic to a release / back to the previous one (200 instant, 202 restarting). Owner/admin login only.
DELETE /v1/releases/:idStop any non-production release; the routed one answers 409.
POST /v1/releases/:id/execRun a command inside the live container (output capped at 1 MiB; the timeout kills the process tree). Owner/admin login only.
POST /v1/apps/:id/domains, POST …/domains/:hostname/verifyBind a custom hostname (unverified, returns the TXT record) / DNS ownership check → routable + TLS.
POST /v1/apps/:id/builds, GET /v1/builds/:id/logsBuild an image from a source snapshot or a base64 zip → 202 / stream build logs; POST /v1/builds/:id/cancel drops a queued build.
GET /v1/apps/limitsQuotas, defaults and the host-port range in force for this workspace.
curl
# Create an application (API key or login)
curl -X POST "https://api.inquir.org/v1/apps" \
  -H "Authorization: Bearer $INQUIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"web","runtime":{"port":3000}}'

# Release a prebuilt image — 202; the pull and the health gate run in the background
curl -X POST "https://api.inquir.org/v1/apps/{appId}/releases" \
  -H "Authorization: Bearer $INQUIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"imageRef":"nginx:alpine"}'
# -> { "id": "…", "status": "PENDING" }   poll GET /v1/releases/{releaseId}

# Promote — needs a user session or personal access token; API keys are refused
curl -X POST "https://api.inquir.org/v1/releases/{releaseId}/promote" \
  -H "Authorization: Bearer $INQUIR_PAT"
# -> { "productionUrls": ["https://web.apps.inquir.org"], "previousReleaseId": "…" }

Limitations

  • One replica per release. A restart — crash recovery, or the recreate that promotion performs when production-only variables are withheld — means a short window of 503 on that release's hostnames. Nothing is cut over on the production hostname until the new container is healthy.
  • No persistent volumes. A container's filesystem lives as long as the container: a database release loses its data when the container is recreated (crash restart, redeploy, rollback restart). Use applications for stateless services and dev/test databases, and keep durable data in a managed store.
  • No autoscaling. Resources are fixed per release (--memory, --cpu); scale by promoting a release with a larger ceiling.