Docs / Working on a Node app
Guidedeploymill://guides/stack/node

Working on a Node app (deploymill)

Reference for agents modifying a deploymill-managed Node app scaffolded from the node template. Covers the layout deploymill assumes, the contract with the platform, and the moves that work cleanly with the rest of the system.

What deploymill scaffolds

  • src/index.js — Hono server using @hono/node-server. Listens on process.env.PORT || 3000. Exposes / (HTML landing) and /healthz ({ok: true}).
  • package.jsontype: "module", start script runs node src/index.js. Hono + @hono/node-server are the only deps. The packageManager field pins the pnpm version corepack uses; don't strip it — corepack would otherwise download the latest pnpm, which can require a newer Node major than the base image ships.
  • Dockerfile — multi-stage build using node:22-alpine + pnpm via corepack. Build stage installs deps (with dev deps) and copies sources; runtime stage carries node_modules, package.json, and src/ only. EXPOSE 3000, CMD ["node", "src/index.js"].

The platform contract

Three things must hold for the deploy to come up:

  1. Bind to 0.0.0.0, not localhost. @hono/node-server's default binds to all interfaces — keep it that way. Localhost-only binding makes the container unreachable from the platform's router.
  2. Listen on process.env.PORT (default 3000). The template already does this; don't hard-code 3000 if you start reading the env elsewhere.
  3. EXPOSE <port> in the Dockerfile must match the listen port. The scaffold pins both to 3000.

Health checks

/healthz returns {ok: true}. The platform doesn't require this route — anything on the published port is routable — but it's the convention deploymill uses for liveness checks. If you remove it, leave a replacement at a stable path.

Common modifications

  • Add a dependency: edit package.json dependencies, then push_files. The build runs pnpm install --prod=false so dev deps install too.
  • Switch to TypeScript: add typescript, @types/node, a tsconfig.json, change the start script to compile-then-node (or use tsx). Update the Dockerfile to compile in the build stage and copy dist/ into the runtime stage. Keep the runtime image free of dev deps.
  • Swap Hono for Express/Fastify: keep the PORT contract and /healthz. The only Hono-specific code lives in src/index.js.
  • Add static files: serve them from the same Node process (Hono's serveStatic) so they live behind the same port and routing.
  • Run a headless background worker (queue consumer, scheduler, etc.): use start_project({ stack: "node", workload: "worker" }). That scaffolds src/templates/worker-node/ — no EXPOSE, no domain, stdout heartbeat as the starter. Replace the heartbeat loop with real recurring work. The worker holds an active-app quota slot while running. For an in-process background task that lives inside the same container as your HTTP server, use setInterval / a process supervisor instead.

Persistent state

Container filesystem is ephemeral. To keep files across deploys (uploads, caches, an on-disk index), add a mount in .deploymill/project.json and read/write under that path. For relational app data, prefer the managed database instead of a volume. See deploymill://guides/storage.

Database

For Postgres, declare database: { provider: "neon" } in .deploymill/project.json, run reconcile_project, then fetch deploymill://guides/database/node and follow it. Don't roll your own connection pool — the guide gives you the one that won't exhaust the database's connection limit.

What NOT to do

  • Don't app.listen(3000, "localhost"). Bind to all interfaces.
  • Don't write to the container filesystem expecting persistence. Use a mount.
  • Don't run pnpm install at container start. It's a build-stage thing. The runtime stage should boot in seconds.
  • Don't hard-code secrets in src/. Use set_env_vars and read from process.env.

Debugging a failed deploy

  • list_deployments shows the most recent deploy and its status.
  • Build + container stdout is available via get_logs({ applicationId }). See deploymill://guides/logs for the full loop. Runtime container logs (stdout/stderr of a running app) aren't yet exposed over REST — use the edge-probe signal and a readiness route to narrow down runtime issues.
  • "App is not responding" usually means: bound to localhost, wrong port, or the process crashed at startup. Run get_logs({ applicationId }) to read the build output.