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, orFailed. 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.
# 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.
| Probe | What it proves |
|---|---|
http | GET 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. |
tcp | The port accepts a connection and keeps it open briefly. |
command | A 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,
drainGraceSecondsper 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:
# 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"
publicpublishes the port beyond loopback and gives the release an endpoint such astcp.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.hostPortpins 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–39999by default; the create form shows the live range) and are a host-wide reservation: a second live release pinning the same port is refused with409until the holder is stopped.- HTTP ports are never published separately — they are served over the hostname (TLS, Host routing, release lifecycle), so
publicandhostPortare rejected on an HTTP port. An application may have no HTTP port at all.
Volumes and databases
A volume is named storage attached to an application: declare it with --volume name:/absolute/path[:ro] (or runtime.volumes[]) and the platform creates one managed Docker volume per application and mounts it there. The physical name is derived from the application, not the release, so the same data volume comes back on every redeploy, crash restart and rollback — and it outlives the application: archiving stops every container and leaves the volume on the host for an operator to remove.
- Only a logical name and an absolute container path are accepted — never a host path, a Docker volume name or driver options — so one workspace cannot mount another's storage. A runtime may declare at most 16 volumes;
/,/dev,/procand/sysare refused. - Declaring a volume flips the deploy to recreate: the running container is stopped before the new one starts, so two writers never share the volume. Expect a short outage on every deploy — and no promote step to confirm, because a recreate release takes production itself as soon as it is healthy.
deployStrategy: rollingbrings the side-by-side deploy back, and is accepted only when every volume is read-only — a writable one is refused with400. - A writable volume pins the application to a single replica (
scaling.maxReplicasabove 1 is refused), and automatic HTTP wake is not offered for an application with volumes at all — scale-to-zero needs an HTTP-only runtime, so a volume application runsalwaysormanual.
That is what the database templates stand on: inquir apps create mydb --template postgres (or --template redis) creates the application with a persistent volume, a pinned public TCP port so the connection string survives every redeploy, a generated password in encrypted env, and its first release — one command from nothing to a running database.
inquir apps connection-url mydb prints the live URL, credentials included. To grant another application access without the password passing through a person, run inquir apps connect mydb web: the server composes the URL and writes it into web's environment as DATABASE_URL (REDIS_URL for redis), where it takes effect on web's next release. The dashboard deliberately shows volumes and their backups, never credentials.
Backups
A backup is a snapshot of one volume's contents in the platform's artifact store, restorable onto that same volume. Backups exist only where the deployment has an object store configured — GET /v1/apps/limits reports it as features.volumeBackups, and the Volumes tab hides the controls when it is off.
- Automatic: every application with a volume and a running production release is snapshotted every 6 hours, keeping the newest 7 snapshots per volume.
- On demand:
inquir apps backups mydb createsnapshots every volume the production release mounts and waits until the rows settle (--no-waitreturns straight away);inquir apps backups mydb listshows them, newest first. - Crash-consistent: the container is paused for the length of the copy, so the archive is a point-in-time image rather than a walk over a data directory that keeps changing underneath it. The copy is read through the running container, so an application that is not running cannot be backed up.
- Restore replaces the volume.
inquir apps backups mydb restore <id> --yesstops the application, replaces the volume's contents with the snapshot and starts it again; everything written since that snapshot is gone. Restoring and deleting a restore point need an owner or admin, like every other action that moves production.
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:
# 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:
| Limit | Default |
|---|---|
| Live releases per workspace (previews + production + reserve) | 10 |
| Memory per release | 512 MB (ceiling 2,048 MB) |
| CPU per release | 0.5 vCPU (ceiling 2 vCPU) |
| Startup health-check deadline | 120 s |
| Drain window (rollback reserve) | 5 min |
| Preview release lifetime | 24 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:….
| Endpoint | Purpose |
|---|---|
POST /v1/apps, GET /v1/apps | Create an application (env values are masked on every read) / list, cursor-paginated. |
GET /v1/apps/:id, PATCH /v1/apps/:id, DELETE /v1/apps/:id | Read (production pointer, URLs, previews, domains, recent releases) / update — masked env values are merged, not overwritten / archive. |
POST /v1/apps/:id/releases | Create 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/logs | Release status projection / SSE container logs. |
POST /v1/releases/:id/promote, POST /v1/apps/:id/rollback | Move production traffic to a release / back to the previous one (200 instant, 202 restarting). Owner/admin login only. |
DELETE /v1/releases/:id | Stop any non-production release; the routed one answers 409. |
POST /v1/releases/:id/exec | Run 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/verify | Bind a custom hostname (unverified, returns the TXT record) / DNS ownership check → routable + TLS. |
POST /v1/apps/:id/builds, GET /v1/builds/:id/logs | Build an image from a source snapshot or a base64 zip → 202 / stream build logs; POST /v1/builds/:id/cancel drops a queued build. |
POST /v1/apps/:id/backups, GET /v1/apps/:id/backups | Snapshot every volume of the production release (<code>202</code> — the copy runs in the background) / list restore points, newest first. |
POST …/backups/:backupId/restore, DELETE …/backups/:backupId | Restore a snapshot over its volume, stopping and restarting the application / drop a restore point. Owner or admin. |
POST /v1/apps/:id/connection-url, POST /v1/apps/:id/connect | The live connection URL of a template database / write it into another application's environment. Both are <code>POST</code> and never cached: this is the one place the API hands back a stored credential. |
GET /v1/apps/limits | Quotas, defaults and the host-port range in force for this workspace. |
# 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": "…" }
The compute plane: an ECS without the ECS
Behind every application sits an orchestration plane in the spirit of AWS ECS — capacity, placement, health and recovery are the platform's job, not yours. Machines (workers) register with the control plane and heartbeat; your releases are placed onto them as fenced allocations; the gateway routes traffic straight to the containers wherever they run.
- Portable releases. Every build is pushed to the platform registry and pinned by digest, so the exact same image starts on any worker — a release is bytes, not a machine.
- Placement. The scheduler picks a worker by capacity (bin-pack on one machine, spread across several); replicas of one application balance per request.
- Health and replicas. A release serves only after its healthcheck passes on the interface traffic actually reaches. Scale
alwaysapps up to 8 replicas; failed containers restart with exponential backoff. - Recovery. A lost worker is detected by lease expiry and its allocations are relocated; worker restarts keep serving containers running and re-adopt them — traffic does not notice.
None of this requires configuration: inquir apps deploy builds, publishes, places and promotes. Adding a machine to the pool is one installer run on a box with Docker — see the runner deployment guide in the repository.
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
503on that release's hostnames. Nothing is cut over on the production hostname until the new container is healthy. - Persistent volumes are local to one runtime host. An application with a volume is pinned to the host that holds it: no replication, no live migration, and backups are the only copy of the data that leaves that host. Removing a mount or archiving the application retains its Docker volume for operator cleanup.
- No autoscaling. Resources are fixed per release (
--memory,--cpu); scale by promoting a release with a larger ceiling.