Skip to main content

Deploy a backend API with secrets and autoscaling

A Node.js API from zero to production: explicit start command, secrets from AWS Secrets Manager, autoscaling, and canary rollouts. The same flow works for Python and Go — variants at the end.

1. The app

Any Node server works; the only requirement is that you know how it starts — Pagifier never reads package.json:

src/server.js
const express = require("express");
const app = express();
app.get("/health", (_, res) => res.send("ok"));
app.get("/", (_, res) => res.json({ hello: "pagifier", db: !!process.env.DATABASE_URL }));
app.listen(3000);

2. The file, explained line by line

pagifier.toml
#:schema https://pagifier.company.com/v1/schema/pagifier.json
version = 1

name = "orders-api"
environment = "prod"
runtime = "node"

build = ["npm ci --omit=dev"] # runs verbatim in the toolchain image

[runtimeConfig]
command = "node src/server.js" # mandatory — never guessed
port = 3000 # mandatory — never guessed

[ingress]
host = "orders.company.com"

healthCheck = "/health" # readiness + liveness probes

[env]
NODE_ENV = "production"

[secrets] # referenced, never stored
provider = "aws"

[secrets.keys] # ENV_VAR = "remote key"
DATABASE_URL = "prod/orders/database-url"

[autoscaling]
enabled = true
minReplicas = 3
maxReplicas = 30
cpu = 70

What each block buys you:

  • runtime = "node" + runtimeConfig — your app runs with exactly this command on exactly this port. A missing command is a 400 at upload, not a broken pod at 2 a.m.
  • secrets — Pagifier renders an ExternalSecret that pulls prod/orders/database-url from AWS Secrets Manager into DATABASE_URL, refreshed hourly. The value never touches Pagifier's API or your repo. (details)
  • autoscaling — an HPA between 3 and 30 replicas at 70% CPU.
  • healthCheck — no traffic reaches a pod until /health returns 200.

If a template exists (template = "express"), the build and runtimeConfig blocks usually come from it and your file shrinks to five lines.

3. Deploy

zip -r app.zip . -x '.git/*' -x 'node_modules/*'
curl -H "Authorization: Bearer $PAGIFIER_TOKEN" \
-F config=@pagifier.toml -F archive=@app.zip \
"https://pagifier.company.com/v1/applications/orders-api/environments/prod/deployments?wait=true"
curl https://orders.company.com/
{"hello":"pagifier","db":true}

"db": true — the secret arrived without ever being written down anywhere.

4. Canary rollouts (probably already on)

If your platform's prod profile enables canaries (most do), your second deploy behaves differently — watch the status while it runs:

curl -H "Authorization: Bearer $PAGIFIER_TOKEN" \
"https://pagifier.company.com/v1/applications/orders-api/environments/prod" | jq .application.canary
{"release": "orders-api-r20260714-…", "weight": 10}

The new version takes 10% of traffic, then 50%, advancing only while error-rate and latency gates pass; a bad release aborts itself and the old version never stopped serving. You did nothing to get this. (how it works)

Python and Go variants

Same file, different runtime — the rules never change (explicit command, explicit port, no manifest inspection):

FastAPI
version = 1
name = "ml-api"
template = "fastapi" # brings pip install + uvicorn + /health probe
environment = "prod"
ingress = "ml.company.com"
Go
version = 1
name = "payments"
template = "go-api" # compiles a static binary, ships ONLY bin/
environment = "prod" # into a distroless image
ingress = "payments.company.com"

Without templates, spell it out — e.g. Go by hand:

runtime = "go"
build = ["mkdir -p bin", "CGO_ENABLED=0 go build -o bin/server ."]
artifact = "bin" # sources never enter the image

[runtimeConfig]
command = "/app/server"
port = 8080