Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.
How to Build and Deploy a Production-Ready Node.js API on Cloud Run
JJhon-HarryPublished inNode.js·APIs·Cloud·
September 24, 2026
·Updated:September 25, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
A Node.js API that works locally is not automatically ready for a cloud environment. Production deployments introduce requirements that are easy to overlook: dynamic ports, health checks, graceful shutdowns, container security, structured logs, and environment-based configuration.
In this tutorial, we’ll build a small Node.js API, package it in a Docker container, test it locally, and deploy it to Google Cloud Run. The same design principles apply to other managed container platforms.
This approach provides a practical foundation for cloud application development because the application remains portable. The cloud platform runs the container, but the service itself does not depend heavily on platform-specific code.
What we’ll build
The example service will provide three endpoints:
GET /returns basic API information.GET /healthreports whether the process is running.POST /tasksvalidates and accepts a task.
- Read configuration from environment variables.
- Listen on the port assigned by the platform.
- Return structured JSON errors.
- Handle termination signals gracefully.
- Run as an unprivileged container user.
You’ll need Node.js 20 or later, Docker, a Google Cloud project, and the Google Cloud CLI if you want to complete the deployment section.
Create the Node.js project
Create a new directory and initialize the project:
mkdir cloud-task-apicd cloud-task-apinpm init -ynpm install express{"name":"cloud-task-api","version":"1.0.0","description":"A containerized Node.js API","type":"module","main":"src/server.js","scripts":{"start":"node src/server.js"},"engines":{"node":">=20"},"dependencies":{"express":"^5.0.0"}}Using an explicit Node.js version makes the runtime requirement visible to developers and build systems.
Build the API
Create a src directory and add src/server.js:
importexpressfrom"express";importprocessfrom"node:process";importcryptofrom"node:crypto";const app =express();const config ={port:Number(process.env.PORT??8080),environment: process.env.NODE_ENV??"development",serviceName: process.env.SERVICE_NAME??"task-api"};app.disable("x-powered-by");app.use(express.json({limit:"32kb"}));app.get("/",(request, response)=>{response.json({service: config.serviceName,environment: config.environment,status:"available"});});app.get("/health",(request, response)=>{response.json({status:"ok",uptimeSeconds:Math.floor(process.uptime())});});app.post("/tasks",(request, response)=>{const{ title, priority ="normal"}= request.body;if(typeof title !=="string"|| title.trim().length<3){return response.status(400).json({error:{code:"INVALID_TITLE",message:"The title must contain at least three characters."}});}const allowedPriorities =newSet(["low","normal","high"]);if(!allowedPriorities.has(priority)){return response.status(400).json({error:{code:"INVALID_PRIORITY",message:"Priority must be low, normal, or high."}});}const task ={id: crypto.randomUUID(),title: title.trim(),priority,createdAt:newDate().toISOString()};console.log(JSON.stringify({severity:"INFO",event:"task_created",taskId: task.id,priority: task.priority}));return response.status(201).json({ task });});app.use((request, response)=>{response.status(404).json({error:{code:"NOT_FOUND",message:"The requested endpoint does not exist."}});});The API limits JSON bodies to 32KB. Request-size limits help protect a public service from unexpectedly large payloads and excessive memory use.
The example also disables Express’s X-Powered-By header. Removing the header is not a complete security measure, but applications generally do not need to advertise their server framework.
Start the server correctly
Add the following code to the bottom of src/server.js:
const server = app.listen(config.port,"0.0.0.0",()=>{console.log(JSON.stringify({severity:"INFO",event:"server_started",port: config.port,environment: config.environment}));});functionshutdown(signal){console.log(JSON.stringify({severity:"INFO",event:"shutdown_started",signal}));server.close(error=>{if(error){console.error(JSON.stringify({severity:"ERROR",event:"shutdown_failed",message: error.message}));process.exit(1);}console.log(JSON.stringify({severity:"INFO",event:"shutdown_complete"}));process.exit(0);});setTimeout(()=>{console.error(JSON.stringify({severity:"ERROR",event:"shutdown_timeout"}));process.exit(1);},10_000).unref();}process.on("SIGTERM",()=>shutdown("SIGTERM"));process.on("SIGINT",()=>shutdown("SIGINT"));Listening on 0.0.0.0 makes the service accessible outside its container. Reading PORT from the environment allows the platform to select the listening port.
The shutdown handlers stop the server from accepting new connections while giving active requests time to finish. The fallback timer prevents the process from waiting indefinitely.
Test the service locally
npm startOpen another terminal and check the health endpoint:
curl http://localhost:8080/healthcurl -XPOSThttp://localhost:8080/tasks -H"Content-Type: application/json" -d '{"title":"Review deployment logs","priority":"high"}'curl -XPOSThttp://localhost:8080/tasks -H"Content-Type: application/json" -d '{"title":"A","priority":"urgent"}'The invalid request should return a 400 status and a predictable JSON error.
Create a production container
FROMnode:20-alpineENVNODE_ENV=productionENVPORT=8080WORKDIR/appCOPYpackage*.json./RUN npm ci --omit=dev && npm cache clean --forceCOPY--chown=node:node src ./srcUSER nodeEXPOSE8080CMD["node","src/server.js"]The image installs only production dependencies and switches to the existing unprivileged node user before starting the service.
node_modulesnpm-debug.log.git.gitignore.envDockerfile*README.mdExcluding local files makes the build context smaller and reduces the chance of copying credentials or development artifacts into the image.
SitePoint’s guide to using Node.js with Docker provides a more detailed introduction to images, containers, bind mounts, and Docker-based development workflows.
docker build -t cloud-task-api .docker run --rm -p 8080:8080 -e SERVICE_NAME=container-task-api cloud-task-apiVisit http://localhost:8080 or repeat the earlier curl commands.
Add a container health check
The /health endpoint can also be used by Docker:
HEALTHCHECK--interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -qO- http://127.0.0.1:8080/health || exit 1Add this instruction before CMD if you need Docker-level health information.
Keep health checks fast and independent of slow external services. A basic liveness endpoint should confirm that the process can respond. A separate readiness check can test whether dependencies required for serving traffic are available.
Deploy the service to Cloud Run
Authenticate the Google Cloud CLI and select your project:
gcloud auth logingcloud config set project YOUR_PROJECT_IDgcloud run deploy cloud-task-api --source . --region us-central1 --allow-unauthenticated --set-env-vars SERVICE_NAME=cloud-task-apiThe official Google Cloud tutorial for building and deploying a Node.js service to Cloud Run explains the required project setup and deployment flow.
The --allow-unauthenticated option makes the example API publicly accessible. Do not use it for private administrative services or endpoints that expose protected data. Configure identity-based access instead.
After deployment, the command prints the service URL. Test it with:
curl https://YOUR_SERVICE_URL/healthKeep configuration outside the image
Deployment-specific settings should be supplied through environment variables rather than written into the container.
For non-sensitive configuration, you can deploy another revision with:
gcloud run services update cloud-task-api --region us-central1 --set-env-vars NODE_ENV=production,SERVICE_NAME=tasksDo not store database passwords, API keys, or signing secrets directly in a Dockerfile,e a managed secret store and grant the service access only to the secrets it requires
Teams using Google Cloud consulting services should still retain ownership of their architecture decisions, deployment configuration, access policies, and operational documentation. External guidance can help with migration or platform design, but the application team must understand how the service is secured, monitored, and recovered.
Improve observability
The example writes single-line JSON objects to standard output. Structured logs are easier for a cloud logging system to parse than inconsistent human-readable messages.
Each log should include useful operational context, such as:
console.log(JSON.stringify({severity:"INFO",event:"request_completed",method: request.method,path: request.path,statusCode: response.statusCode,durationMs}));Avoid logging authorization headers, cookies, request bodies, personal information, or secrets.
In production, useful measurements include:
- Request count and latency.
- Error rate by endpoint and status.
- Container startup time.
- Memory and CPU use.
- Active instance count.
- Failed deployment revisions.
Create alerts around symptoms that affect users, not merely around individual log messages.
Avoid storing state inside the container
Managed containers should be treated as disposable. A platform may start or stop instances as demand changes, and local files may disappear with the instance.
Store durable information in an external database or object-storage service. Do not use in-memory arrays or local JSON files as the authoritative data store for sessions, orders, tasks, or user records.
The API in this tutorial returns the created task but does not persist it. A production version should validate the request and then write the task to a managed database through a dedicated data-access layer.
Production checklist
Before releasing a cloud service, verify that:
- The application listens on the platform-provided port.
- Secrets are supplied through a managed secret store.
- The container runs as an unprivileged user.
- Request bodies have size limits.
- Inputs are validated on the server.
- Logs exclude sensitive values.
- Shutdown signals are handled.
- Durable state is stored outside the container.
- Public access is intentional.
- Deployment and rollback procedures are documented.
- Alerts cover latency, availability, and error rates.
Also pin and regularly update runtime and dependency versions. Run automated tests and a container build in continuous integration before deploying a new revision.
Final thoughts
Deploying a Node.js API to a managed container platform is straightforward, but production readiness depends on decisions made before the deployment command runs.
A portable container, environment-based configuration, structured logging, input validation, graceful shutdown, and external state storage create a stronger foundation than platform-specific shortcuts. With those practices in place, the service becomes easier to test locally, deploy consistently, monitor in production, and move if its infrastructure requirements change.


