Backend Apps
Backend Apps
A Backend App is a long-running server that Ngris builds in an isolated sandbox and runs in-cluster, reached over global HTTPS through an endpoint. Ship a Go, Node, or Python service — or any stack that comes with a Dockerfile — and Ngris compiles it into a signed, scanned, digest-pinned image and runs it for you. No agent, no CLI, no server of your own to operate.
Backend Apps come in two shapes, both carrying a backend Ngris runs:
- Backend only (type
managed) — a monolith server; every path is served by your backend. - Full-stack (type
fullstack) — a static front end on the edge plus a path-routed backend (a Vercel-style split:/api/*hits the backend, everything else serves the static front end). See Full-stack apps.
Backend Apps require the application_hosting entitlement, plus managed-backend availability on your plan (the managed_backends entitlement — the on/off master gate). Without it, create returns 403 Managed backends are not available on your plan.
How a deploy works
Every deploy takes your source from code to a live URL through the same pipeline:
- Upload a
.zipof your source, or connect a Git repo so every push redeploys. - Ngris auto-detects your language — Go, Node, or Python — and builds it onto an Ngris-pinned minimal base; or, if your source has a
Dockerfile, it builds that instead. - The build runs in a hardened, gVisor-sandboxed job and produces a signed, vulnerability-scanned, digest-pinned container image.
- Ngris runs that image in a per-app sandboxed pod and serves it via its endpoint. Because the image is immutable and pinned to its digest, rolling back to an earlier version is instant.
A deploy moves through a series of statuses you can watch live: queued → building → image_ready → rolling_out → ready (or failed). Open Deploys → the deploy → Log and the build log tails live while it runs, so you can see exactly what ran and why a build broke.
Your build context is the zip root
This is the single most common thing that trips up a first backend deploy, so read it before you zip.
The Docker build context is the root of your uploaded zip. Every Dockerfile COPY path is relative to that root. Your Dockerfile must sit at the root of the zip, and the zip must be self-contained: no COPY ../…, no paths that assume a parent or monorepo directory, and no reliance on sibling directories that aren’t inside the zip. If a path points outside the context, the build fails.
The monorepo-prefix trap
If your Dockerfile was written to build from the root of a monorepo (e.g. COPY myapp/web/… run from the repo root) but you zip just the app, the myapp/ prefix no longer exists inside the context and the build fails with something like:
failed to compute cache key: lstat /myapp/web/package.json: no such file or directory
The fix is to drop the prefix so paths are relative to the zip root:
# BEFORE — assumes a monorepo root that isn't in the zip COPY myapp/web/package.json ./ COPY myapp/web/ ./ # AFTER — relative to the zip root (the build context) COPY web/package.json ./ COPY web/ ./
Local modules must be vendored into the zip
If your app depends on a local module via a replace directive — a monorepo sibling like a shared/ package that lives outside the app directory — that sibling is not in the context, so the build can’t resolve it. Copy the module into the zip and point the replace at the in-zip copy:
# zip layout: put the sibling INSIDE the app dir before zipping # myapp/ # go.mod # main.go # shared/ <-- vendored copy of the local module # BEFORE — replace points OUTSIDE the zip (a monorepo sibling) # go.mod: replace example.com/shared => ../shared # AFTER — vendor it in, and point the replace at the in-zip copy COPY . ./ RUN go mod edit -replace=example.com/shared=./shared \ && go build -o /app/server ./...
The rule is simple: if the build needs it, it must be inside the zip. The build context has no access to your machine, your parent directories, or the rest of a monorepo — only what you uploaded.
What to put in the zip
Ship source, not build output — the build reinstalls and rebuilds your dependencies in the sandbox. A lean zip builds faster and avoids surprises. Exclude:
node_modulesand any vendored dependency trees — the build installs them fresh (npm ci,go mod download,pip install).- Compiled or
dist/buildoutput — it’s regenerated inside the image. .git— history isn’t needed to build.- Any
.envwith secrets — a committed.envcan override the platform-injected config, and secrets don’t belong in an image. Set them as environment variables instead.
The cleanest way to enforce all of the above is a .dockerignore at the zip root:
node_modules dist build .git .env *.log
node_modules or build output you should exclude.
Environment variables & secrets
Set config, tokens, and connection strings under the app’s Environment settings (or the API). Ngris injects each variable into the running container’s process environment under its exact name — your app reads it with the normal env API of its language (os.Getenv("DATABASE_URL"), process.env.DATABASE_URL, os.environ["DATABASE_URL"]).
- Mark sensitive values as Secret — they’re encrypted at rest, masked in the API and dashboard (write-only), and decrypted only inside the pod at start-up.
- Changes take effect on your next deploy or rollout — trigger one with a push, a re-upload, or by re-activating the current deploy.
- Names must match
[A-Za-z_][A-Za-z0-9_]*.
curl -X PUT "https://api.ngris.com/v1/applications/{uuid}/env" \
-H "X-API-KEY: <your_api_key>" \
-H "Content-Type: application/json" \
-d '{"name":"LOG_LEVEL","value":"info","is_secret":false}'
Need a database? A Backend App can add a fully-managed MySQL-compatible database that auto-injects its connection (DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, and a ready-to-use DATABASE_URL) as environment variables — nothing to install or copy. See App databases.
The port your app must listen on
The platform routes traffic to the port you configure for the app, so your server must listen on that port. Bind it on all interfaces — 0.0.0.0:<port>, not 127.0.0.1 — or requests won’t reach it.
Set the port in the app’s Runtime settings (or PUT /v1/applications/{uuid}/runtime with port, valid 1–65535). If you don’t set one, the default expected port is 8080.
8080, 3000, 8000, or any port ≥ 1024 — never 80 or 443. HTTPS is terminated for you at the endpoint.
Other runtime knobs live alongside the port: a health check path (health_check_path, default /), CPU & memory (cpu_millis 50–4000, memory_mb 64–8192; requests equal limits for Guaranteed QoS), and replicas (min_replicas, max_replicas).
Filesystem: read-only root, writable /tmp
The container runs with a read-only root filesystem. The only writable location is /tmp, a small scratch mount (~256 Mi).
- Write any temp files — caches, uploads in flight, scratch output — only to
/tmp. Point your framework’s temp dir there (TMPDIR=/tmp) if it defaults elsewhere. - Don’t try to write to the app directory, the working directory, or anywhere else at runtime — those writes fail. Bake everything the app needs at run time into the image at build time.
/tmpis ephemeral and per-pod — it’s wiped on restart and not shared between replicas. Persist real data in a managed database or an external store, never on the local filesystem.
Deploy a backend in 5 steps
Dashboard walkthrough
- Create the app. Open Applications → New and pick type Backend (or Full-stack if it also serves a static front end).
- Set env vars & secrets. Add your config under Environment, marking sensitive values as Secret. Add a managed database from the Database tab if you need one.
- Upload or connect. Choose Upload and drop your source zip (self-contained, ≤ 100 MB), or Connect Git so every push redeploys.
- Watch the build. Open the deploy’s Log — it tails live — and wait for the deploy to reach
ready. - It’s live. The app serves at its endpoint URL. Add a custom domain any time — TLS is provisioned automatically.
API — create a backend app
curl -X POST "https://api.ngris.com/v1/applications" \
-H "X-API-KEY: <your_api_key>" \
-H "Content-Type: application/json" \
-d '{"name":"my-api","type":"managed","source":"upload"}'
Use "type":"fullstack" for a static front end plus a path-routed backend, or "source":"git" with a repo_url to connect a repo instead of uploading.
API — upload the source & deploy
curl -X POST "https://api.ngris.com/v1/applications/{uuid}/deploys" \
-H "X-API-KEY: <your_api_key>" \
-F "file=@my-api-src.zip" \
-F "version=v1.0.0" # optional label
The build runs asynchronously. Poll GET /v1/applications/{uuid}/deploys until the newest deploy reaches ready, or stream the build log with GET /v1/applications/{uuid}/deploys/{deploy_uuid}/logs. Read live runtime status — instances, ready replicas, restarts — from GET /v1/applications/{uuid}/status.
Something wrong?
COPY path, a missing dependency, or a server that never bound its port. Walk the fixes in Deploy troubleshooting.