Node.js API on Cloud Run: Build, Deploy, Harden
To build, deploy, and harden a Node.js API on Cloud Run, you containerize the app, push it to Artifact Registry, deploy with a service account and resource limits, then apply security and reliability practices like managed TLS, IAM, and secrets. This article walks through the full lifecycle, from project setup to production hardening, with concrete commands and configuration examples.
Background: Why Cloud Run for Node.js APIs
Cloud Run is a managed compute platform on Google Cloud that runs stateless containers in a serverless model. It scales automatically from zero to thousands of instances, charges only for the time requests are being processed (with a minimum billable time), and integrates with the rest of the Google Cloud ecosystem. For Node.js APIs, it removes the need to manage Kubernetes clusters or virtual machines, while still allowing custom runtimes and dependencies.
Key characteristics that matter for production:
- Autoscaling based on incoming requests, down to zero when idle
- Built-in HTTPS certificates and custom domain mapping
- Per-request billing, with a minimum of 100 ms per request
- Support for any language, including Node.js, as long as it runs in a container
- Integration with Cloud SQL, Cloud Storage, and other GCP services via IAM
Compared to Cloud Functions, Cloud Run gives you more control over the runtime, supports longer timeouts (up to 60 minutes), and allows background threads (though they are not guaranteed). It is a good middle ground between full container orchestration and single-function serverless.
How It Works: The Build and Deploy Pipeline
The typical flow uses Cloud Build to compile a Docker image, then deploys that image to Cloud Run. Here is a minimal but realistic pipeline.
Project structure:
my-api/
Dockerfile
package.json
src/
index.js
.gcloudignore
Dockerfile example:
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . ENV PORT=8080 EXPOSE 8080 CMD ["node", "src/index.js"]
Using npm ci instead of npm install ensures a clean, reproducible install. The --only=production flag omits dev dependencies, reducing image size and attack surface.
Deploy with gcloud:
gcloud builds submit --tag gcr.io/PROJECT_ID/my-api:v1 gcloud run deploy my-api --image gcr.io/PROJECT_ID/my-api:v1 --platform managed --region us-central1 --allow-unauthenticated
The first command builds the image and pushes it to Google Container Registry (or Artifact Registry if configured). The second creates a service named my-api with the specified image. The --allow-unauthenticated flag makes the endpoint publicly accessible; for production you often want to remove this and use IAM or an API gateway.
Hardening for Production: Security and Reliability
Production hardening goes beyond the basic deploy. Here are the essential areas to address.
1. Least-Privilege Service Account
By default, Cloud Run uses the default compute service account, which has broad permissions. Create a dedicated service account with only the roles the API needs. For example, if it reads from Cloud Storage:
gcloud iam service-accounts create my-api-sa --display-name="My API Service Account" gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:my-api-sa@PROJECT_ID.iam.gserviceaccount.com" --role="roles/storage.objectViewer" gcloud run services update my-api --service-account=my-api-sa@PROJECT_ID.iam.gserviceaccount.com
Apply the principle of least privilege: grant only the permissions required, and avoid using the default compute engine service account.
2. Secrets and Environment Variables
Do not bake secrets into the container image or pass them as plain environment variables. Use Secret Manager to store sensitive values like database passwords or API keys. Then reference them in Cloud Run:
gcloud secrets create my-db-password --data-file=password.txt gcloud run services update my-api --set-secrets="DB_PASSWORD=my-db-password:latest"
In your Node.js code, access it via process.env.DB_PASSWORD. Cloud Run injects the secret value at runtime. This keeps secrets out of the image and allows rotation without redeploying.
3. Resource Limits and Concurrency
Set CPU and memory limits to prevent a single instance from exhausting resources. Also set concurrency to control how many requests a single instance handles simultaneously. For a typical Node.js API, a concurrency of 80 is a common starting point, but you should load-test to find the optimal value.
gcloud run services update my-api --memory=512Mi --cpu=1 --concurrency=80
Note: Cloud Run charges for the entire instance lifetime when you set CPU to be always allocated. For request-driven APIs, you can set CPU to be allocated only during request processing (the default) to save costs, but be aware that background work outside requests is not guaranteed.
4. Health Checks and Startup Probes
Cloud Run sends requests to the container’s port. Implement a /health endpoint that returns 200 when the app is ready to serve traffic. You can configure startup and readiness probes:
gcloud run services update my-api --health-check-path=/health --startup-check-path=/health
This helps Cloud Run know when your container is ready and avoids sending traffic to a cold instance that is still loading dependencies.
5. Logging and Monitoring
Cloud Run automatically collects logs and metrics, but you need to ensure your Node.js app writes structured logs. Use console.log with JSON objects, or a logging library like pino. Set up alerts for 5xx errors, high latency, or container restarts.
const logger = require('pino')();
logger.info({ path: req.path }, 'request received');
Cloud Run integrates with Cloud Monitoring, so you can create dashboards and alerts based on request counts, error rates, and instance count.
How to Choose: Configurations and Options
When deploying, you have several choices that affect performance and cost.
- CPU allocation: Always allocated vs. only during request. The latter is cheaper but may delay cold starts.
- Minimum instances: Set to 1 or more to avoid cold starts, but you pay for those instances even when idle.
- Region: Choose a region close to your users to reduce latency.
- Custom domain: Map a custom domain to get a clean URL and easier TLS management.
- VPC connector: If your API needs to reach resources in a VPC (e.g., a private Cloud SQL instance), attach a VPC connector.
For a production API, start with a minimum of 1 instance to avoid cold-start latency, but monitor usage and adjust based on traffic patterns.
Common Problems and Misconceptions
Cold starts: A common complaint is that Cloud Run has cold starts. Yes, when scaling from zero, the first request may take a few seconds. Mitigate by setting a minimum instance count, but understand the cost trade-off.
File system is read-only: The container’s filesystem is ephemeral and writable only in the /tmp directory, which is also ephemeral. Do not store state in the filesystem; use Cloud Storage or a database.
Background tasks: If you set CPU to be allocated only during requests, background timers or promises may not run reliably. For background jobs, either use Cloud Tasks or set CPU to always allocated.
Port mismatch: Cloud Run expects your container to listen on the port defined by the PORT environment variable (default 8080). If your app hardcodes a different port, it will fail.
Costs and Scaling
Cloud Run pricing is based on three factors: instance time (CPU and memory), number of requests, and network egress. The free tier includes a generous amount of each, but beyond that you pay per vCPU-second and GiB-second. For a low-traffic API, costs can be under a few dollars per month. For high traffic, it scales linearly, but you can control costs by setting concurrency and instance limits.
Autoscaling is automatic: Cloud Run adds instances as request concurrency exceeds the configured limit, and removes them when demand drops. The maximum number of instances is configurable (default 100) and can be set to a lower value to prevent runaway costs.
Alternatives and Comparisons
Cloud Run is not the only option for deploying a Node.js API on Google Cloud. Here are common alternatives and when to choose them.
- Cloud Functions: Simpler for single-purpose functions, but limited runtime (up to 60 minutes) and less control over the environment.
- Google Kubernetes Engine (GKE): More control and flexibility, but requires cluster management and has a learning curve.
- Compute Engine VM: Full control over the OS and runtime, but you manage updates, scaling, and availability.
- App Engine: Similar serverless model but with a different scaling model and some platform-specific constraints.
For most new Node.js APIs that need to scale automatically without operational overhead, Cloud Run is a strong choice. If you need to run long-lived connections (like WebSockets) or require GPU access, you may need GKE or Compute Engine.
Frequently Asked Questions
How do I set up a custom domain with TLS?
In the Cloud Run console, go to your service, select the “Domain mappings� tab, and add your domain. Google automatically provisions an SSL certificate via Let’s Encrypt. You then update your DNS records with the provided CNAME or A record. After propagation, your API will be accessible at your domain with HTTPS.
Can I use WebSockets on Cloud Run?
Yes, but with caveats. Cloud Run supports WebSocket connections, but they count as long-running requests. If your API needs to hold many WebSocket connections, you must set concurrency to 1 (or a low number) because each connection occupies an instance. This can become expensive. For heavy WebSocket usage, consider GKE or a dedicated solution.
How do I handle database connections with Cloud Run?
Use a connection pool (e.g., pg with a pool) and set the maximum connections per instance to a number that accounts for concurrency. For example, if concurrency is 80, you might set the pool max to 20 to avoid exhausting the database. Also, use Unix sockets if your database is in the same VPC to reduce overhead.
What is the maximum request timeout?
Cloud Run allows request timeouts up to 60 minutes. The default is 5 minutes. Set the timeout based on your API’s needs, but be aware that long timeouts tie up instances and increase costs.
How do I roll back to a previous revision?
Each deploy creates a new revision. To roll back, go to the Cloud Run console, select the service, click on “Revisions� tab, and choose the revision you want to restore. Then click the “Manage traffic� button and set 100% traffic to that revision. You can also do this via gcloud: gcloud run services update-traffic my-api --to-revisions=REVISION=100.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.