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 onprocess.env.PORT || 3000. Exposes/(HTML landing) and/healthz({ok: true}).package.json—type: "module",startscript runsnode src/index.js. Hono +@hono/node-serverare the only deps. ThepackageManagerfield 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 usingnode:22-alpine+ pnpm via corepack. Build stage installs deps (with dev deps) and copies sources; runtime stage carriesnode_modules,package.json, andsrc/only.EXPOSE 3000,CMD ["node", "src/index.js"].
The platform contract
Three things must hold for the deploy to come up:
- Bind to
0.0.0.0, notlocalhost.@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. - Listen on
process.env.PORT(default 3000). The template already does this; don't hard-code 3000 if you start reading the env elsewhere. 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.jsondependencies, thenpush_files. The build runspnpm install --prod=falseso dev deps install too. - Switch to TypeScript: add
typescript,@types/node, atsconfig.json, change thestartscript to compile-then-node (or usetsx). Update the Dockerfile to compile in the build stage and copydist/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 insrc/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 scaffoldssrc/templates/worker-node/— noEXPOSE, 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, usesetInterval/ 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 installat container start. It's a build-stage thing. The runtime stage should boot in seconds. - Don't hard-code secrets in
src/. Useset_env_varsand read fromprocess.env.
Debugging a failed deploy
list_deploymentsshows the most recent deploy and its status.- Build + container stdout is available via
get_logs({ applicationId }). Seedeploymill://guides/logsfor 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.