To deploy server.js to production, the minimum bar is: a package.json with a start script, a port the platform can set via process.env.PORT, a /health endpoint that returns 200, an env-var contract the platform injects, a Dockerfile or build command the platform runs, and a deploy pipeline that does not require SSH. The first 24 hours after the deploy are the part the tutorials skip — the health check failing, the env vars not loading, the platform’s default port not matching the app’s hardcoded 3000, the database connection failing because the platform’s network is not the team’s network. This post walks through the first 24 hours in order.
This is not a tutorial for the first deploy — the first deploy is one push to main on a platform that handles the build. This is a tutorial for what to do when the first deploy is up and the team has to keep it up. The first 24 hours are where most production incidents happen, and the patterns below are the ones that prevent them.
Table of contents
- The minimum bar before the first deploy
- The first deploy, in five steps
- The first 24 hours: the five things that go wrong
- The env var contract most teams get wrong
- The health check that the platform will use to roll you back
- The first incident, and the runbook template
- When to move off the platform’s free tier
- FAQ
The minimum bar before the first deploy
Before pushing to main, the codebase needs four things:
- A
package.jsonwith astartscript that runs the server. The platform will runnpm start(or whatever the build command is). Thestartscript has to bind toprocess.env.PORT, not a hardcoded 3000. The platform sets the port; the app reads it. - A
/healthendpoint that returns 200. The endpoint has to do the minimum amount of work — no database call, no third-party API call, no auth check. The platform uses this endpoint to decide when to roll back a broken deploy. - An env var contract. The team has to know which env vars the app reads at startup (database URL, API keys, signing secrets) and which platform to inject them through. Hardcoded secrets in the repo are out. Secrets in the dashboard are in.
- A build command that does not require SSH. The platform’s build is a fresh checkout of the repo. The build has to install dependencies and run any pre-build steps without interactive prompts. A
Dockerfileor apackage.jsonbuildscript is the standard shape.
If the codebase has all four, the first deploy is one push. If it does not, the first deploy is a day of debugging.
The first deploy, in five steps
- Pick the platform. The platforms that handle Node.js well are Render, Railway, Fly.io, Vercel (for the API routes), AWS App Runner, and a dozen smaller ones. The right answer is the one whose pricing model matches the team’s expected traffic and whose Node.js support is in the base plan.
- Connect the repo. The platform reads the GitHub or GitLab repo on push. The first push is the first deploy.
- Set the build and start commands. The build command is usually
npm install. The start command isnpm start. If the platform needs a Dockerfile, the Dockerfile is the build command. - Add the env vars. The platform’s dashboard has a “Environment” section. Add the database URL, the API keys, the signing secrets. The platform injects them at build time and at runtime.
- Push to
main. The platform builds, deploys, and runs the new version. The deploy log is the team’s view into what happened.
That is the first deploy. The first 24 hours are next.
The first 24 hours: the five things that go wrong
The five most common first-day failures, in order of frequency:
- The app crashes on startup because the port is hardcoded. Symptom: the deploy log shows the app started, then exited with
EADDRINUSEorlisten EADDRINUSE. Fix: bind toprocess.env.PORT || 3000instead of3000. - The health check fails because the endpoint requires auth. Symptom: the deploy log shows the app started, the platform’s health check returns 401, the platform restarts the app in a loop. Fix: make
/healtha public endpoint that returns 200 without auth or DB calls. - The database connection fails because the app does not have the right URL. Symptom: the deploy log shows the app started, the first request returns 500, the log shows
ECONNREFUSEDorpassword authentication failed. Fix: check the env var, check the database’s network rules, check that the platform and the database are in the same network region. - The build fails because of a missing dependency. Symptom: the deploy log shows
npm installfailing on a peer dependency or a missing package. Fix: commit the lockfile (package-lock.json) and usenpm ciinstead ofnpm installin the build command. - The deploy succeeds but the service does not respond. Symptom: the deploy log shows success, the URL returns 502 or times out. Fix: check the platform’s health check, check the app’s logs for the actual error, and roll back to the previous version if the error is not obvious.
Each of these is a pattern, not a one-time bug. The fix for each is a config change, a code change, or a deployment-process change. None of them is “switch platforms.”
The env var contract most teams get wrong
The env var contract is the list of variables the app reads at startup. The contract has three rules:
- The app reads from
process.env, never from a.envfile in production. The.envfile is for local development. The platform injects the real values at runtime. - The platform has a dashboard where the team sets the values. The values are not in the repo, the values are not in the build artifact, the values are not in the deploy log. The values are in the platform’s secret store.
- The team has a list of which env vars the app needs, with the names exactly matching. The team writes the list in a
README.mdor in a.env.examplefile. The next engineer to onboard reads the list and sets the values in the dashboard.
The most common mistake is the team using dotenv to load the .env file in production. The .env file is for local development. In production, the platform injects the values. The dotenv call works locally and silently fails in production, and the team does not notice until the first 500.
The health check that the platform will use to roll you back
The health check is the platform’s way of asking “is this app healthy?” The platform calls the endpoint, expects a 200 within a timeout, and rolls back the deploy if the health check fails. The endpoint has to be:
- Fast. No database calls, no third-party API calls, no auth checks. A 200 within 10ms is the right answer.
- Public. No auth, no rate limiting, no IP allowlist. The platform’s IP is not the team’s IP, and the platform does not want to maintain a list of allowed IPs.
- Stable. The endpoint does not return 500 unless the app is actually broken. A 500 on a transient database blip is the wrong answer; the platform will roll back a deploy that was actually fine.
The standard pattern is:
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
Three lines. No database call. No auth. No third-party call. Returns 200 always. The platform uses this to decide when to roll back.
The first incident, and the runbook template
The first incident is going to happen. The most common first incident is the database connection failing because the platform restarted the app and the new instance does not have the right env var. The second most common is the third-party API the app depends on rate-limiting the team’s IP. The third is a deploy that the team did not realize was happening.
The runbook for the first incident has four steps:
- Open the deploy log. The deploy log is the first place to look. The log shows what the app did at every step of the deploy and at every step of the request.
- Open the runtime log. The runtime log shows what the app did after the deploy. The log shows the database error, the third-party API error, the auth error, or the actual exception.
- Roll back to the previous version. The platform has a “rollback” or “redeploy previous version” button. Roll back to confirm the previous version was the working one.
- Fix the issue in the code, commit, push. The fix is in the repo, not in the dashboard. Push the fix, the platform redeploys, the new version is the new working version.
The runbook is the same for the second incident, the third incident, and the hundredth incident. The pattern is: log, log, rollback, fix, push. The team’s job is to make the pattern fast.
When to move off the platform’s free tier
The free tier is the right answer for the first few months. The first month is the build. The second month is the first users. The third month is the first scaling problem. The fourth month is the conversation about whether the free tier is still the right answer.
The signals that the free tier is no longer the right answer:
- Cold starts are visible to the user. The free tier often has cold starts that the team’s traffic is not big enough to keep warm. The first user after a quiet period sees a 5-second delay. That is the signal.
- The platform is hitting the team’s resource limits. The platform shows a warning: “you have used 80% of your free tier bandwidth this month.” That is the signal.
- The team needs a feature the free tier does not have. Custom domains, private networks, dedicated IPs, audit logs, role-based access — these are the features the free tier skips. The team needs one of them. That is the signal.
The move to the paid tier is a config change, not a migration. The platform keeps the deploys, the database, the env vars. The team’s bill goes up, the team’s features go up, and the team’s architecture does not change.
How this fits the rest of the stack
The deploy decision is also a cost decision. The deploy minutes, the build minutes, the database, the storage, the bandwidth, the workers, and the secrets each show up as a line item on the platform bill, and the team’s mental model for the project cost is the sum of those numbers. The right answer is to know the line items before the project ships, not after. The RunxBuild hosting calculator is the right place to do that exercise — pick the runtime size, the database tier, the storage, the bandwidth, the build frequency, and the secrets, and the calculator shows what the deploy actually costs at the team’s actual usage.
Useful related references:
FAQ
How do I deploy server.js to production?
The minimum bar: a package.json with a start script, a port the platform sets via process.env.PORT, a /health endpoint that returns 200, an env var contract the platform injects, a Dockerfile or build command the platform runs, and a deploy pipeline that does not require SSH. Most teams push to a managed platform that handles the build, the database, the deploy logs, the health checks, and the rollback.
What port should server.js listen on?
process.env.PORT, with a fallback for local development. Hardcoding 3000 in production is the most common first-day failure — the platform sets the port, the app ignores the platform’s port, the app crashes on startup, and the deploy log shows EADDRINUSE.
Do I need a Dockerfile to deploy a Node.js app?
No, but it depends on the platform. Render, Railway, and Fly.io all support a “build from package.json” mode that does not require a Dockerfile. AWS App Runner and most Kubernetes-based platforms require a Dockerfile. The right answer is to use a Dockerfile if the team’s stack is non-trivial, and to use the platform’s native mode if the team’s stack is a standard Node.js app.
What is a health check and why does the platform need one?
A health check is an endpoint the platform calls to decide if the app is healthy. The standard pattern is GET /health returning 200 within a timeout. The platform uses the health check to decide when to roll back a broken deploy. The endpoint has to be fast, public, and stable — no database calls, no auth, no third-party calls.
How do I roll back a bad deploy?
The platform has a “rollback” or “redeploy previous version” button. The button redeploys the last working version. The team uses the rollback to confirm the previous version was the working one, then fixes the issue in the code, commits, and pushes. The new version is the new working version.
What env vars does a Node.js app need in production?
It depends on the app. The minimum is usually the database URL, a session secret, and any third-party API keys. The team has a list in a README.md or in a .env.example file. The values are set in the platform’s dashboard, never in the repo, never in the build artifact, never in the deploy log.
When should I move from the free tier to the paid tier?
The signals: cold starts are visible to the user, the platform is hitting the team’s resource limits, or the team needs a feature the free tier does not have (custom domains, private networks, dedicated IPs, audit logs). The move to the paid tier is a config change, not a migration.