Deploy a NetScript workspace
Goal: take the workspace you scaffolded with netscript init and run it somewhere
other than your laptop — a container host, a VM, or a managed platform — with clear
expectations about what the scaffold wires for you and what you still own.
This is a task recipe, not a one-click button. NetScript is in alpha, and the scaffold is
deliberately minimal about deployment: it gives you a single declarative description of every
process (appsettings.json), runnable Deno entrypoints with explicit permissions, the Aspire
AppHost that orchestrates them locally and can publish deployment artifacts for targets you
configure in it, and starter GitHub Actions workflows for the shipped deployment targets. The CLI
has thin deploy routers for Deno Deploy, Docker/Compose, Kubernetes, Azure, and Cloud Run, but it
does not generate a Dockerfile, a docker-compose.yml, or a finished cloud infrastructure
stack for you — target-specific infrastructure still lives in Aspire and your cloud account,
assembled from the verified facts below.
Before you start
You need a working, type-checked workspace and a clear idea of where it is going. If you have not built one yet, start with Quickstart and the Storefront tutorial. Then confirm the workspace is healthy locally before you try to move it:
deno task check # type-check apps, services, contracts
deno task lint
deno task test
For the orchestration model that underpins everything below — why the AppHost lives in its
own aspire/ folder, and how it provisions Postgres and Redis — read the
Aspire explanation alongside this recipe.
The mental model: three layers
A NetScript deployment is three layers, and you choose how much of each you keep in production:
Postgres (the recommended database; mysql, mssql, or sqlite are first-class alternatives via --db) and Redis (KV/cache — the default --cache-backend; garnet and deno-kv are alternatives). In dev, Aspire provisions Postgres/MySQL/SQL Server as containers (sqlite is file-backed, no container) and Redis as a container. In production you bring your own — managed database, managed Redis-compatible cache.
Each API service, plugin service, background processor, and the Fresh app is one Deno process with an entrypoint, a port, and an explicit permission set — all declared in appsettings.json.
Aspire's AppHost wires the graph together locally. In production you can keep Aspire (it can publish a deployment manifest) or drop it and run each process yourself with your own supervisor.
Step 1 — Know your deployable units (appsettings.json)
appsettings.json is the single source of truth for what runs. The CLI writes it during
netscript init and updates it as you netscript plugin install. Every Aspire resource — and
every process you would deploy by hand — is described there. From a workspace with the four
first-party plugins installed, the graph looks like this:
| Name | Type | Description |
|---|---|---|
users |
service · :3000 (the scaffold default; the exact port is OS-allocated from the SERVICE range starting at 3000) |
Example oRPC service. Entrypoint src/main.ts, runtime deno. RPC mounts under /api/rpc/*. |
streams |
plugin · :4437 |
durable-streams runtime service. RequiresDb=false, RequiresKv=false. Real producer runtime — see Step 6. |
workers-api |
plugin · :8091 |
Workers API. Requires DB + KV. References streams. |
sagas-api |
plugin · :8092 |
Sagas API. Requires DB + KV. References workers-api, streams. |
triggers-api |
plugin · :8093 |
Triggers API. Typed v1 oRPC contract for trigger/event introspection + management; the webhook ingress endpoint POST /api/v1/webhooks/:triggerId stays a raw HMAC-verifying route by design. Requires DB + KV. References workers-api, streams. |
workers / sagas |
background processor |
Entrypoint bin/combined.ts. Watch mode + telemetry on. Workers runtime pool via WORKERS_CONCURRENCY; sagas via SAGA_CONCURRENCY. |
triggers |
background processor |
Entrypoint src/runtime/trigger-processor.ts. Concurrency 10 via TRIGGER_CONCURRENCY. |
dashboard |
app · :8010 |
Fresh frontend. References service users. |
postgres / redis |
infrastructure |
Mode=Container in dev. PrimaryDatabase=postgres, PrimaryCache=redis (the default; garnet via --cache-backend garnet). |
Each entry carries the exact information a deploy needs: Runtime, Port, Entrypoint,
Workdir, RequiresDb/RequiresKv, the Deno Permissions array, the concurrency env var,
and PluginReferences (the wiring order). When you containerize or write systemd units, copy
these values verbatim — do not guess them.
Step 2 — Build and validate a release artifact
There is no opinionated build step that produces a single bundle — each Deno process runs from source. Your "build" is therefore: cache dependencies, type-check, and (optionally) pre-generate the database client and plugin registries so the container does not do it at boot.
# From the workspace root — prove the graph is healthy before you ship it.
deno task check
deno task lint
deno task test
# Warm the module cache so production start-up does no network fetch.
# Cache each deployable entrypoint you intend to run.
deno cache services/users/src/main.ts
deno cache plugins/workers/services/src/main.ts
deno cache plugins/sagas/services/src/main.ts
deno cache plugins/triggers/services/src/main.ts
# Requires Aspire (and therefore Postgres) up first — see the DB callout below.
# Bake the Prisma client + plugin registries into the artifact so boot is deterministic.
netscript db generate
netscript generate plugins
Step 3 — Provision backing services
In production you do not run Postgres and Redis as throwaway Aspire containers. You
provision them as durable, managed resources and hand their connection details to NetScript
through environment variables. NetScript reads the database URL from POSTGRES_URI (falling
back to DATABASE_URL) and normalizes engine-specific connection strings to a URL — this is
handled in database/postgres/prisma.config.ts.
| Name | Type | Description |
|---|---|---|
POSTGRES_URI |
string (url) |
Primary Postgres connection. DATABASE_URL is the accepted fallback. Read by Prisma config. |
REDIS_URI / cache url |
string |
Redis-compatible cache endpoint for the redis KV/cache resource (the default backend). With --cache-backend garnet the key is GARNET_URI for the garnet resource (managed Redis or Garnet in prod). |
PORT |
number |
Per-process listen port. Each service reads it (e.g. Deno.env.get('PORT') ?? '8091') and falls back to its default. |
OTEL_EXPORTER_OTLP_ENDPOINT |
string (url) |
OTLP collector. Dev defaults to http://localhost:4318 (http/protobuf) via the Aspire dashboard. |
NETSCRIPT_SAGA_STORE |
kv | prisma |
Durable saga store backend (mandatory when sagas run). Also settable via appsettings sagas.store.backend. |
NETSCRIPT_AUTH_BACKEND |
string |
Active auth backend if the auth plugin is installed. Default kv-oauth. |
WORKERS_CONCURRENCY |
number |
Workers runtime process pool size. Current Aspire metadata also emits WORKER_CONCURRENCY, but the runtime honors WORKERS_CONCURRENCY; set the runtime var. |
SAGA_CONCURRENCY |
number |
Sagas background processor concurrency (default 2). |
TRIGGER_CONCURRENCY |
number |
Triggers background processor concurrency (default 10). |
Step 4 — Choose an orchestration path
This is the real fork in the road. Pick based on whether your target understands Aspire.
# The AppHost is a TypeScript/Node project under aspire/ (apphost.mts).
# Locally it provisions Postgres + Redis and wires every process.
cd aspire
aspire restore # one-time: restore the TS AppHost SDK
aspire start # boots the full graph; dashboard at https://localhost:18888
# Aspire 13.x can publish/deploy target artifacts from the same AppHost.
netscript deploy kubernetes plan --project-root .
netscript deploy kubernetes up --project-root .
# Scaffold without the orchestration layer, then run processes yourself.
netscript init my-app --no-aspire
# You now own provisioning Postgres + a cache, and starting each process.
# Bring-your-own-supervisor: systemd, a container per process, or a PaaS.
# Start the Fresh app directly during dev:
deno task --cwd apps/dashboard dev
Aspire cloud targets
The Aspire-backed target routers all share the same shape:
# Generate artifacts without applying them.
netscript deploy kubernetes plan --project-root . --output-dir .deploy/kubernetes
# Apply/provision through the validated AppHost platform integration.
netscript deploy azure-aks up --project-root . --output-dir .deploy/azure-aks
# Tear down the previously deployed target.
netscript deploy azure-aks down --project-root . --output-dir .deploy/azure-aks
For Kubernetes, add the Aspire Kubernetes integration to the TypeScript AppHost
(aspire add kubernetes, then builder.addKubernetesEnvironment('k8s')). Use
publishAsKubernetesService(...) in aspire/apphost.mts for per-service manifest
customization such as replicas, labels, annotations, or extra manifests. plan
runs aspire publish --apphost <path> --output-path .deploy/kubernetes; Aspire
emits a Helm chart with Chart.yaml, values.yaml, and templates/.
Apply the published chart with your normal cluster workflow:
kubectl config current-context
helm upgrade --install my-netscript-app .deploy/kubernetes
# Or inspect/apply rendered manifests if your release process requires kubectl.
helm template my-netscript-app .deploy/kubernetes > .deploy/kubernetes/rendered.yaml
kubectl apply -f .deploy/kubernetes/rendered.yaml
For Azure, configure the matching AppHost hosting integration before using the
router. The CLI validates the AppHost source contains an Azure Container Apps,
Azure App Service, or Azure Kubernetes marker before it shells
aspire publish|deploy --apphost <path>. Azure CLI login, subscription/location
parameters, provider feature registration, and RBAC are operator prerequisites.
For Cloud Run, configure the Docker-image provider fields:
export default defineConfig({
// ...
deploy: {
targets: {
'cloud-run': {
registry: 'us-docker.pkg.dev/acme/prod',
imageName: 'orders-api:latest',
},
},
},
});
netscript deploy cloud-run up runs docker build -t <registry>/<imageName> ., docker push <registry>/<imageName>, then gcloud run deploy <service> --image <registry>/<imageName> --quiet.
Step 5 — Wire generated CI and promotion
New Aspire-backed workspaces include three GitHub Actions starter workflows under
.github/workflows/:
| Name | Type | Description |
|---|---|---|
deploy-compose-ghcr.yml |
Compose + GHCR |
Restores the Aspire AppHost, emits Compose output with netscript deploy compose plan, builds/pushes images to GHCR, then runs netscript deploy docker up with --clear-cache. |
deploy-deno-deploy.yml |
Deno Deploy |
Runs workspace checks, then calls netscript deploy deno-deploy up using GitHub secrets and variables for the Deno Deploy token, organization, and app. |
deploy-bare-metal.yml |
Bare metal |
Compiles service artifacts with netscript deploy build on Linux and Windows runners, then uploads the output as workflow artifacts for host-specific installation. |
Treat the workflow environment input as the promotion ladder:
development first, then staging, then production. In GitHub, map those names to protected
environments and keep secrets environment-scoped so a staging run cannot accidentally read
production credentials. Promote the same reviewed commit through each environment; do not rebuild
from a different branch between staging and production.
Step 6 — Run a process by hand (the bare-metal primitive)
Under every option above, the atomic unit is the same: one Deno process started from an
entrypoint with the exact permission set from appsettings.json. This is what a container
CMD, a systemd ExecStart, or a PaaS start command ultimately becomes. For the workers API
(:8091), it is:
# Run from the workspace root. Flags and entrypoint come straight from appsettings.json.
PORT=8091 \
deno run \
--unstable-kv --allow-net --allow-env --allow-read --allow-write --allow-run \
plugins/workers/services/src/main.ts
The corresponding background processor (which actually executes jobs) runs its own entrypoint:
# Workers + sagas background processors share bin/combined.ts; triggers uses its own.
WORKERS_CONCURRENCY=2 \
deno run \
--unstable-kv --allow-net --allow-env --allow-read --allow-write --allow-run \
workers/bin/combined.ts
Map this pattern across every enabled resource and you have a complete, container-free
deployment. To containerize, each process becomes one image whose CMD is the matching
deno run line; orchestrate them with compose or your platform of choice, honoring the
PluginReferences start order (streams → workers → sagas/triggers, plus auth-api when present).
Step 7 — Verify the deployment
Once your processes are up against real backing services, hit the health endpoints to confirm the graph is wired. These are the exact routes the local runtime exposes (substitute your production host):
| Name | Type | Description |
|---|---|---|
GET /health |
:8091 |
Workers API health. |
GET /health/live |
:8092 |
Sagas API liveness. |
GET /health |
:8093 |
Triggers API health (Hono). |
GET /api/v1/workers/jobs |
:8091 |
Lists registered worker jobs — proves the jobs registry generated. |
GET /api/v1/sagas/sagas |
:8092 |
Lists registered sagas — proves saga metadata is in KV. |
POST /api/v1/webhooks/inbound/generic |
:8093 |
Inbound webhook → enqueues the workers health-check job (end-to-end proof). |
GET /api/v1/events?limit=10 |
:8093 |
Recent trigger events. |
GET /api/v1/auth/session |
:8094 |
Auth session probe (only if the auth plugin is installed). |
(dashboard) |
https://localhost:18888 |
Aspire dashboard: every resource, health, logs, distributed traces (Aspire path only). |
# Smoke a deployed graph (replace localhost with your host).
curl -fsS http://localhost:8091/health
curl -fsS http://localhost:8092/health/live
curl -fsS http://localhost:8093/health
curl -fsS "http://localhost:8091/api/v1/workers/jobs"
If every health endpoint returns and /api/v1/workers/jobs lists your jobs, the processes are
running with their permissions, reaching Postgres/KV, and discovering their registries — the
deployment is live.
Where to go next
For the full generated API of each deployable unit, see the reference: workers, sagas, triggers, and streams. For every CLI command grouped by workflow, see the CLI reference.