# Node, Python, Rails and Go on Cloud Run: the container and the traps of each stack

> The container each of Node, Python, Rails and Go needs on Cloud Run, and the traps we reproduced: dropped requests on shutdown, one-at-a-time gunicorn, port 80, and Go's CPU count.

Published 2026-10-12 · Updated 2026-10-13 · [By Exodus team](https://exodus.elim.io/about) · Stacks, Patterns
<https://exodus.elim.io/blog/node-python-rails-and-go-on-cloud-run>

Node, Python, Rails and Go all run on Cloud Run when the container listens on the port in PORT (8080), exits within 10 seconds of SIGTERM, logs JSON to stdout and keeps nothing on disk. Each stack breaks one rule by default: npm start drops requests, gunicorn's defaults serve one request at a time, Rails 8 listens on port 80, and a go line below 1.25 counts the host's CPUs.

## In short

- In our test, `npm start` cut a 3-second request in flight; `node server.js` in the exec form answered it and exited in 2.8 seconds.
- Gunicorn's defaults answered 8 requests of 3 seconds in 24.0 seconds; 1 worker with 8 threads took 3.1 seconds.
- The Dockerfile of Rails 8.1.4 listens on port 80, runs `db:prepare` at every start and uses SQLite: set `HTTP_PORT=8080`, migrate in a job, use Postgres.
- On a 1-CPU container, a `go 1.22` line gave GOMAXPROCS 20 and `go 1.25` gave 2.
- Time to the first answer: Go 271 ms, Node 328 ms, Python 503 ms, Rails 3,077 ms.

## The rules every stack shares

We built a small service in Node.js, Python, Ruby on Rails and Go, ran each with 1 CPU and 512 MiB, the size of a default Cloud Run instance, and reproduced the traps one by one. Rust has [its own guide](https://exodus.elim.io/blog/rust-on-cloud-run), and the choice between Google's buildpacks and your own Dockerfile is in [buildpacks or a Dockerfile](https://exodus.elim.io/blog/buildpacks-or-a-dockerfile).

![Four boxes, one per stack, under a shared rule: listen on PORT at 0.0.0.0, exit on SIGTERM within 10 seconds, log JSON to stdout, keep nothing on disk. Node: CMD node server.js in exec form with a SIGTERM handler. Python: exec gunicorn with 8 threads and timeout 0, PYTHONUNBUFFERED. Rails: HTTP_PORT 8080, migrations in a job, Postgres and the master key as a secret. Go: go 1.25 in go.mod, wait for Shutdown, a static binary](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-4fd3e4c99b473dfdc63bf9c0ca802c180cb9357abee212342e0a9b94c90cfb8a-680x420-svg/bytes "One contract for every container, and the line each stack has to change to keep it")

Cloud Run's container contract asks four things, whatever the language ([what Cloud Run is](https://exodus.elim.io/blog/what-cloud-run-is) covers the platform itself).

**Listen on the port in `$PORT`, on all interfaces.** Requests arrive on 8080 unless you change it. A server bound to `localhost` cannot be reached from outside its container, and the deploy fails with "Container failed to start and listen on the port defined by PORT=8080".

**Stop cleanly on SIGTERM.** When Cloud Run removes an instance, it sends SIGTERM, waits 10 seconds and sends SIGKILL. Your process should stop taking connections, finish its requests and exit. The catch is a Linux rule: the first process in a container is process 1, and it receives a signal only if it has installed a handler for it. A server with no handler, or a shell that started your server and does not pass signals on, ignores SIGTERM and dies 10 seconds later from SIGKILL. In a Dockerfile, the shell form `CMD node server.js` runs your command under `/bin/sh -c`; the exec form `CMD ["node", "server.js"]` runs it directly. Use the exec form, or put `exec` in front of the command when a shell must expand a variable.

**Write logs to stdout, one JSON object per line.** Cloud Run sends stdout and stderr to Cloud Logging, and a JSON line with `severity` and `message` fields becomes an entry you can filter by level ([logs and alerts](https://exodus.elim.io/blog/logs-metrics-and-alerts-for-a-cloud-run-app)).

**Keep nothing on the local disk.** The filesystem is in memory and belongs to one instance, so uploads, SQLite files and caches written there use memory and vanish with it ([patterns that break](https://exodus.elim.io/blog/patterns-that-break-on-cloud-run)).

One setting matters for every stack: **concurrency**, the number of requests Cloud Run sends to one instance at once. A service created with gcloud gets 80 per vCPU, and Google's advice is to set it no higher than what your server is configured to handle, which differs by stack ([concurrency](https://exodus.elim.io/blog/concurrency-on-cloud-run)).

## Node.js

Node runs your JavaScript on one thread and handles many requests by not blocking on I/O, so the default concurrency of 80 suits a typical API. CPU-heavy work blocks every request on the instance, and a single-threaded app cannot use a second vCPU, so start at 1 vCPU. A minimal server that keeps the contract:

```ts
import http from "node:http";

const port = Number(process.env.PORT) || 8080;
const server = http.createServer(async (req, res) => {
  if (req.url === "/slow") await new Promise((r) => setTimeout(r, 3000));
  res.end(req.url === "/slow" ? "done\n" : "hello\n");
});
server.listen(port, "0.0.0.0", () =>
  console.log(JSON.stringify({ severity: "INFO", message: `listening on ${port}` })));

// Node as PID 1 ignores SIGTERM unless it has a handler.
process.on("SIGTERM", () => {
  console.log(JSON.stringify({ severity: "INFO", message: "SIGTERM, finishing requests in flight" }));
  server.close(() => process.exit(0));
});
```

With Express or Fastify the shape is the same: keep the object `listen` returns and close it on SIGTERM. The Dockerfile:

```dockerfile
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci \
    --omit=dev
COPY . .
USER node
# exec form: node is PID 1 and gets SIGTERM itself
CMD ["node", "server.js"]
```

Add `node_modules` to `.dockerignore`, so local modules do not overwrite the installed ones.

**The trap is how the server is started.** We started the same image three ways and stopped it with a 3-second request in flight. With `node server.js` in the exec form, the request got its 200 and the container exited cleanly in 2.8 seconds. With `npm start`, npm forwarded SIGTERM to the shell it runs scripts in, exited with an error, and the server went down with the container before its handler ran: the request got no answer, after 0.9 seconds. With the shell form, the request finished, but Node never saw SIGTERM and was killed after 10.3 seconds. Google's Node page recommends `node` over `npm` for start time, and Node's Docker guide says npm swallows the signal. Google's example is written in the shell form, so take the advice and write the exec form.

![Terminal output of run.sh: node server.js finished its request and stopped in 2,815 ms; npm start lost the request and exited 1 in 891 ms; the shell form was killed after 10,332 ms; gunicorn defaults took 24,024 ms for eight 3-second requests and returned 500 after 30,021 ms; Rails exited without secret_key_base and listened on port 80; Go reported GOMAXPROCS 20 with go 1.22 and 2 with go 1.25, and lost a request when main exited before Shutdown](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-31677a6b1602db7f39ca4750c0d9d0148112f129b1a1befd9a7460654f830b3f-680x440-svg/bytes "The traps, reproduced on our build machine with 1 CPU and 512 MiB per container")

A source deploy without a Dockerfile runs the `start` script of your `package.json`, and there Cloud Run replaces `npm start` with a direct `node` call for you.

Two numbers are worth knowing. At 512 MiB, Node 24 allowed a heap of 259 MiB; at 256 MiB it still allowed 259 MiB, more than the container has, so a large heap ends in an out-of-memory kill rather than a JavaScript error. Below 512 MiB, or when the heap needs more than half the memory, set the limit to about three quarters of the instance. With a flag to pass, the shell form with `exec` keeps node as process 1:

```dockerfile
CMD exec node \
    --max-old-space-size=384 \
    server.js
```

And since Node 18 the HTTP server gives a client 300 seconds to send its whole request, so an upload slower than that fails even with an hour-long Cloud Run timeout ([timeouts](https://exodus.elim.io/blog/long-requests-and-timeouts)).

## Python

Python web apps run under a separate server: gunicorn for Flask and Django, uvicorn for FastAPI. The server's settings decide how many requests an instance handles, and gunicorn's defaults are one worker process with one thread. Google's Python page gives the command; here is the Dockerfile around it:

```dockerfile
FROM python:3.14-slim
# print() reaches the logs at once, not when a buffer fills
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install \
    --no-cache-dir \
    -r requirements.txt
COPY . .
RUN useradd \
    --create-home app
USER app
# exec: gunicorn replaces the shell, becomes PID 1 and gets SIGTERM
CMD exec gunicorn \
    --bind :$PORT \
    --workers 1 \
    --threads 8 \
    --timeout 0 \
    main:app
```

The shell form expands `$PORT`, and `exec` keeps the shell from holding the signal: without it, our container was killed after 10.3 seconds.

**The first trap is concurrency.** We sent 8 requests that each wait 3 seconds, like calls to another API. Gunicorn's defaults answered them one after another in 24.0 seconds; with 8 threads, all 8 took 3.1 seconds. On Cloud Run all 8 can land on one instance, since 8 is under the default concurrency of 80, so the last user waits 24 seconds and the instance is billed for all of it.

![Bar chart: eight 3-second requests arriving at once keep a gunicorn instance with default settings busy for 24.0 seconds, and an instance with one worker and 8 threads for 3.1 seconds](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-c375b90a64415d1e08b1398f131b7c98be9ccae11c283cfe12ff5f5e16edc05b-680x360-svg/bytes "Gunicorn's defaults serve one request at a time; Cloud Run bills the instance for as long as any request is open")

Match the two settings: Google says Python needs as many threads as the concurrency you allow, so with 1 worker and 8 threads deploy with a concurrency of 8:

```shell
gcloud run deploy api \
  --source . \
  --region=europe-west1 \
  --concurrency=8
```

Google suggests one worker per vCPU first, then threads, measuring after each change, since workers cost memory and start time.

**The second trap is the timeout.** Gunicorn kills a worker silent for 30 seconds. Our 35-second request got a 500 at 30.0 seconds with "WORKER TIMEOUT" in the log, while Cloud Run allows 5 minutes by default. A timeout of 0 leaves it to Cloud Run.

**The third is buffering.** When stdout is not a terminal, Python buffers `print()`. Without `PYTHONUNBUFFERED`, three requests that each printed a line left nothing in the log; with it, all three lines were there.

Two smaller ones. A source deploy without a Dockerfile starts `gunicorn -b :8080 main:app` (Python 3.12 and earlier, or no server named in `requirements.txt`), which has both traps above; set your own command in a Procfile. And gunicorn listens before it has loaded your app, so the default startup check passes early; gunicorn's preload option or a [startup probe](https://exodus.elim.io/blog/health-checks-and-startup-probes) closes that gap. For FastAPI, run `uvicorn main:app` with host `0.0.0.0` and port `$PORT`.

## Rails

`rails new` writes a production Dockerfile, and since Rails 8 it puts Thruster, a small proxy for compression and static files, in front of Puma. A source deploy uses a Dockerfile when it finds one, so `gcloud run deploy` on a fresh Rails 8 app builds that file. We generated an app with Rails 8.1.4 and ran its image unchanged. Four things stood in the way.

**It listens on port 80.** The image ends with `EXPOSE 80` and starts `./bin/thrust ./bin/rails server`. Thruster listens on `HTTP_PORT`, 80 by default, and starts Puma on 3000, so nothing answers on 8080 and the deploy fails with "Container failed to start". In our run the logs said `"http":":80"`. Add one line to the final stage of the Dockerfile:

```dockerfile
# Thruster listens here; Cloud Run sends requests to 8080
ENV HTTP_PORT=8080
```

**It cannot start without its secret.** Rails in production needs `secret_key_base`, which normally lives in the encrypted credentials file opened with `RAILS_MASTER_KEY`. Without it the container exited with "Missing `secret_key_base` for 'production' environment". Keep the key in Secret Manager and pass it at run time ([secrets](https://exodus.elim.io/blog/secrets-on-cloud-run)). Google's Rails tutorial passes it into the image as a build argument, which leaves the key in the image for anyone who can pull it.

**It has no production database.** The generated `config/database.yml` uses SQLite, with the production paths commented out under a warning that SQLite needs a persistent disk; the app would not start until we set them. On Cloud Run those files, and the cache, queue and Action Cable databases Rails 8 adds, would live in one instance's memory. Use Postgres on Cloud SQL through the socket Cloud Run mounts ([Cloud SQL](https://exodus.elim.io/blog/connecting-cloud-run-to-cloud-sql)), and a Cloud Storage bucket for Active Storage.

```shell
rails new myapp \
  --database=postgresql
```

**It migrates the database at every start.** The generated `bin/docker-entrypoint` runs `rails db:prepare` before the server. On Cloud Run that happens on every new instance: a scale-up to 5 instances runs it five times, a failing migration crash-loops all of them, and each cold start pays for it. Our first answer came after 2.8 to 3.1 seconds with it and 1.8 to 2.1 without. Delete that block and run migrations as a job from the same image before the deploy ([migrations as jobs](https://exodus.elim.io/blog/database-migrations-as-cloud-run-jobs)):

```shell
gcloud run jobs create migrate \
  --image=europe-west1-docker.pkg.dev/PROJECT_ID/app/web:v2 \
  --region=europe-west1 \
  --set-cloudsql-instances=PROJECT_ID:europe-west1:db \
  --set-secrets=RAILS_MASTER_KEY=rails-master-key:latest \
  --command=./bin/rails \
  --args=db:migrate \
  --execute-now \
  --wait
```

Then the service. The generated `config/puma.rb` runs one process with 3 threads (`RAILS_MAX_THREADS`), so match the concurrency to it:

```shell
gcloud run deploy web \
  --image=europe-west1-docker.pkg.dev/PROJECT_ID/app/web:v2 \
  --region=europe-west1 \
  --add-cloudsql-instances=PROJECT_ID:europe-west1:db \
  --set-secrets=RAILS_MASTER_KEY=rails-master-key:latest \
  --concurrency=3 \
  --cpu-boost
```

If you raise `RAILS_MAX_THREADS`, raise the concurrency too; the generated database pool follows the same variable. Rails is the slowest of the four to start, so startup CPU boost, extra CPU while an instance starts, is worth its small cost ([min instances and boost](https://exodus.elim.io/blog/min-instances-and-startup-cpu-boost)). Rails 8 logs to stdout in production without the `RAILS_LOG_TO_STDOUT` variable Google's tutorial mentions, in plain text rather than JSON.

## Go

Go compiles to one static binary, so the image can be almost empty. Two stages:

```dockerfile
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod ./
COPY *.go ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /hello .

# no shell and no libc: the static binary runs as a nonroot user
FROM gcr.io/distroless/static-debian13:nonroot
COPY \
    --from=build /hello /hello
ENTRYPOINT ["/hello"]
```

With dependencies, copy `go.sum` too and run `go mod download` before copying the code, so that layer is cached. Our image was 3.8 MB compressed.

**The first trap was in our own code.** The usual Go shutdown calls `srv.Shutdown` from a goroutine when the signal arrives. But `ListenAndServe` returns the moment `Shutdown` starts, and if `main` returns right after it, the process exits with requests still running. Our first version did that: the request in flight got no answer, and the container stopped in 0.36 seconds. The `net/http` documentation warns about it. The fix is a channel `main` waits on:

```
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
done := make(chan struct{})
go func() {
	<-ctx.Done()
	shutdown, cancel := context.WithTimeout(context.Background(), 9*time.Second)
	defer cancel()
	srv.Shutdown(shutdown)
	close(done)
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
	log.Error(err.Error())
	os.Exit(1)
}
// ListenAndServe returns as soon as Shutdown starts; wait for the requests in flight.
<-done
```

With it, the request got its 200 and the container exited in 3.0 seconds. Our full sample also logs JSON through `log/slog`, with the level key renamed to `severity`.

**The second trap is GOMAXPROCS**, the number of threads that run Go code at once. Before Go 1.25 it defaulted to the CPUs of the machine, not the container's limit, so a 1 vCPU service on a large host scheduled work for every core and got throttled. Go 1.25 reads the CPU limit, rounds it up and never goes below 2. But the new default depends on the `go` line in `go.mod`, not on the compiler. On our 20-core machine, with the container held to 1 CPU and Go 1.27 building both, `go 1.22` gave 20 and `go 1.25` gave 2. Raise the line or set `GOMAXPROCS` yourself, and log the value at start to see what the instance reports.

For memory, `GOMEMLIMIT` tells the garbage collector when to work harder. It is a soft limit, and the Go guide suggests 5 to 10 percent of headroom, so about `460MiB` on a 512 MiB instance. Our service used 10 MiB under load, so many services never need it.

## Start time, memory and what it costs

The same small service in four stacks, each held to 1 CPU and 512 MiB, started very differently:

![Bar chart of the median time from docker run to the first 200 response: Node 24 328 ms, Python 3.14 503 ms, Rails 8.1 3,077 ms, Go 1.27 271 ms; under each bar the compressed image size and memory under load: 81.4 MB and 31 MiB, 49.6 MB and 46 MiB, 182.1 MB and 126 MiB, 3.8 MB and 10 MiB](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-50f501dd632cb51acafefacb2ba09fa7313be6bd9c4848bb0f633ba521894b0a-680x360-svg/bytes "A small service in each stack, five starts each on our machine. A real app with more code and a database connection starts slower")

These are local numbers for a service that does almost nothing; across three runs they ranged Node 328 to 485 ms, Python 503 to 626, Rails 2,845 to 3,077, Go 271 to 328. On Cloud Run the instance is created first, and your app's own work, loading code and opening database connections, comes on top; Google's Node and Python pages suggest loading heavy modules only when a request needs them. The order is what the chart shows: a Rails start is the one users are most likely to notice.

Memory is cheap. At list prices of October 12, 2026 in a Tier 1 region such as europe-west1, request-based billing costs $0.000024 per vCPU-second and $0.0000025 per GiB-second, so the 512 MiB of a 1 vCPU instance is 5% of its price. Rails, the largest, used 126 MiB under load.

Concurrency is where the stack changes the bill. Cloud Run bills an instance for the time it has at least one request open, so overlapping requests on one instance are billed once. Take 100,000 requests a month that each wait 3 seconds and arrive 8 at a time. Served one at a time, as gunicorn's defaults do, the instance is busy for 300,000 seconds: $7.62 at list price, or $2.88 after the monthly free tier of 180,000 vCPU-seconds if nothing else uses it. Served by 8 threads, it is busy for 37,500 seconds: $0.99 at list price, inside the free tier. [Costs by traffic](https://exodus.elim.io/blog/cloud-run-costs-worked-examples-by-traffic) works through larger cases.

## What breaks

- **A server on `localhost` or a fixed port:** "Container failed to start".
- **`npm start`, a shell-form CMD, or no SIGTERM handler in Node:** requests cut, or a 10-second wait for SIGKILL.
- **Gunicorn's defaults:** one request at a time, and a 500 after 30 seconds.
- **Python without `PYTHONUNBUFFERED`:** log lines missing.
- **A Rails 8 image as generated:** port 80, no production database, `db:prepare` at every start.
- **A Rails master key baked into the image:** anyone who can pull it has the key.
- **A Go `main` that returns before `Shutdown`:** requests in flight dropped.
- **A `go` line of 1.24 or older:** GOMAXPROCS counts the host's CPUs.

## Checklist

1. The server reads `PORT` and listens on `0.0.0.0`.
1. The server starts in exec form (or after `exec`) and handles SIGTERM: locally, `docker stop` returns well under 10 seconds and a request in flight is answered.
1. Logs are JSON lines on stdout with `severity` and `message`; Python has `PYTHONUNBUFFERED=1`.
1. Cloud Run concurrency matches the server: 80 for Node and Go, workers times threads for gunicorn, `RAILS_MAX_THREADS` times workers for Puma.
1. Timeouts inside the app (gunicorn, Node) are at least the Cloud Run timeout.
1. Nothing on local disk: databases on Cloud SQL, files in Cloud Storage.
1. Secrets come from Secret Manager at run time, not from the image.
1. Rails: `HTTP_PORT=8080`, no `db:prepare` in the entrypoint, migrations in a job.
1. Go: `go 1.25` or later in `go.mod`, and `main` waits for `Shutdown`.

## Where Exodus comes in

The Exodus CLI reads a repository and reports what will break on Cloud Run before you move. On the generated Rails app, `exodus audit` named the stack (Ruby 3.4, Rails 8, Solid Queue, SQLite), found the web process in the Dockerfile with its entrypoint and Thruster, and listed SQLite as a data store, and `exodus check` flagged Active Storage writing to the local disk, with a Cloud Storage bucket as the fix. It did not flag the port, the migration at start, `npm start` or gunicorn's defaults, so the checklist above is still yours to run.

## FAQ

### Why does my Cloud Run deploy fail with "Container failed to start and listen on the port"?

The server listens on localhost or on a port other than the one in PORT, 8080 by default. Bind to 0.0.0.0 and read PORT. A Rails 8 image as generated has the same failure: Thruster listens on port 80 until HTTP_PORT is set to 8080.

### How many gunicorn workers and threads should I run on Cloud Run?

Google's Python page starts gunicorn with 1 worker, 8 threads and a timeout of 0, and Cloud Run's concurrency should then be 8, the workers times the threads. Add a worker per vCPU before more threads, and measure after each change, since workers cost memory and start time.

### Should a Dockerfile for Cloud Run start Node with npm start?

No. In our test npm start lost a request in flight when the container was stopped, and a shell-form CMD left Node without SIGTERM until it was killed after 10.3 seconds. Start node server.js in the exec form of CMD and close the server on SIGTERM.

### Does Go set GOMAXPROCS from the Cloud Run CPU limit?

Since Go 1.25 it does, rounding the limit up and never going below 2, but only when the go line in go.mod is 1.25 or later. With a go 1.22 line our 1-CPU container reported 20, the host's cores. Raise the line or set GOMAXPROCS yourself.

### Should Rails run its migrations when the container starts on Cloud Run?

No. The generated entrypoint runs db:prepare on every new instance, so a scale-up to 5 instances runs it five times and a failing migration crash-loops all of them. Delete that block and run db:migrate as a Cloud Run job from the same image before the deploy.

## Sources

1. [Container runtime contract, Cloud Run, Google (requests arrive on PORT, 8080 by default; SIGTERM to every container, then 10 seconds, then SIGKILL; the filesystem is in memory; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/container-contract)
2. [Select an execution environment for services, Cloud Run, Google (install a SIGTERM handler; a container that does not catch SIGTERM is still given 10 seconds; wrapping the service with tini adds a handler; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/execution-environments)
3. [Graceful shutdowns on Cloud Run: deep dive, Google Cloud blog (a shell-form ENTRYPOINT runs the app under /bin/sh -c, and shells do not forward signals; use the vector form; read Oct 12, 2026)](https://cloud.google.com/blog/topics/developers-practitioners/graceful-shutdowns-cloud-run-deep-dive)
4. [Optimize Node.js applications for Cloud Run, Google (start with node, not npm start, because npm adds latency; a source deploy without a Dockerfile does it for you; bundling and lazy imports; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/tips/nodejs)
5. [Optimize Python applications for Cloud Run, Google (exec gunicorn with 1 worker, 8 threads and timeout 0; gunicorn listens before the app is loaded, so the default startup probe passes early; PYTHONUNBUFFERED for logs; slim images; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/tips/python)
6. [Maximum concurrent requests for services, Cloud Run, Google (up to 1,000; 80 per vCPU by default from gcloud; start source deploys at about 8; Node.js is single-threaded; Python needs as many threads as the concurrency; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/about-concurrency)
7. [General development tips, Cloud Run, Google (set Cloud Run concurrency equal to or below the concurrency your code is configured for; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/tips/general)
8. [Configure CPU limits for services, Cloud Run, Google (a single-threaded application uses one vCPU, and above 1 vCPU CPU-based autoscaling misreads it; start at 1 vCPU; startup CPU boost adds CPU while an instance starts; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/services/cpu)
9. [Building a Node.js application, Google Cloud's buildpacks (the buildpack runs scripts.start or npm start; production dependencies only; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/docs/buildpacks/nodejs)
10. [Build a Python application, Google Cloud's buildpacks (default entrypoint gunicorn -b :8080 main:app for Python 3.12 and earlier or when no server is named; uvicorn for FastAPI on 3.13 and later; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/docs/buildpacks/python)
11. [Building a Ruby application, Google Cloud's buildpacks (the entrypoint comes from the Procfile's web line or GOOGLE_ENTRYPOINT; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/docs/buildpacks/ruby)
12. [Deploy services from source code, Cloud Run, Google (a Dockerfile in the source directory is used when present; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/deploying-source-code)
13. [Running Rails on the Cloud Run environment, Google (Rails 8 or later; Cloud SQL for PostgreSQL, the master key in Secret Manager, migrations as a Cloud Run job; the tutorial passes the master key into the image as a Docker build argument; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/ruby/rails/run)
14. [Troubleshoot Cloud Run issues, Google (Container failed to start and listen on the port defined by PORT=8080; gunicorn's default timeout and WORKER TIMEOUT; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/troubleshooting)
15. [Configure request timeout for services, Cloud Run, Google (5 minutes by default, 60 minutes at most; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/request-timeout)
16. [Logging and viewing logs in Cloud Run, Google (stdout and stderr go to Cloud Logging; a line of serialized JSON is parsed into jsonPayload, with severity and message read from it; updated Oct 9, 2026, read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/logging)
17. [Cloud Run pricing, Google (request-based, Tier 1: $0.000024 per vCPU-second, $0.0000025 per GiB-second, $0.40 per million requests; free each month 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests per billing account; read Oct 12, 2026)](https://cloud.google.com/run/pricing)
18. [gcloud CLI reference, Google Cloud SDK 587.0.0 (run deploy with --command, --args, --port, --concurrency, --set-secrets, --set-env-vars, --cpu-boost, --add-cloudsql-instances; run jobs create with --command, --args, --set-cloudsql-instances, --set-secrets, --execute-now, --wait; read with --help on Oct 12, 2026)](https://docs.cloud.google.com/sdk/gcloud/reference/run/deploy)
19. [Docker and Node.js best practices, nodejs/docker-node (Node was not designed to run as PID 1; put node in CMD so npm does not swallow SIGTERM; read Oct 12, 2026)](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md)
20. [HTTP, Node.js documentation (server.requestTimeout is 300 seconds since v18 and covers receiving the whole request; read Oct 12, 2026)](https://nodejs.org/api/http.html)
21. [Settings, gunicorn (timeout 30 seconds by default, 0 disables it; threads 1 by default, and more than 1 switches the sync worker to gthread; read Oct 12, 2026)](https://gunicorn.org/reference/settings)
22. [Signal handling, gunicorn (TERM is a graceful shutdown; read Oct 12, 2026)](https://gunicorn.org/signals)
23. [sys, Python documentation (stdout is block-buffered when not interactive; PYTHONUNBUFFERED or -u makes it unbuffered; read Oct 12, 2026)](https://docs.python.org/3/library/sys.html)
24. [Tuning performance for deployment, Rails Guides (Puma's generated config sets 3 threads through RAILS_MAX_THREADS; workers match cores; read Oct 12, 2026)](https://guides.rubyonrails.org/tuning_performance_for_deployment.html)
25. [Thruster README, Basecamp (HTTP_PORT defaults to 80; TARGET_PORT 3000, and Thruster sets PORT to it for Puma; read Oct 12, 2026)](https://github.com/basecamp/thruster)
26. [Go 1.25 release notes and runtime package documentation, the Go project (GOMAXPROCS follows the cgroup CPU limit, rounded up and never below 2; the old behavior stays for modules whose go line is 1.24 or below; GOMEMLIMIT; read Oct 12, 2026)](https://pkg.go.dev/runtime)
27. [Container-aware GOMAXPROCS, the Go blog (why a CPU limit and GOMAXPROCS differ; read Oct 12, 2026)](https://go.dev/blog/container-aware-gomaxprocs)
28. [A guide to the Go garbage collector, the Go project (GOMEMLIMIT is a soft limit; leave 5-10% headroom; read Oct 12, 2026)](https://go.dev/doc/gc-guide)
29. [net/http, Server.Shutdown, the Go project (ListenAndServe returns ErrServerClosed at once; the program must wait for Shutdown to return; read Oct 12, 2026)](https://pkg.go.dev/net/http#Server.Shutdown)
30. Exodus team: four sample services (Node 24.21, Python 3.14.8 with Flask 3.1.3 and gunicorn 26.2.0, the Dockerfile generated by rails new 8.1.4 with Puma 8.0.2 and Thruster, Go 1.27.2 on distroless) run on our build machine with 1 CPU and 512 MiB each, Oct 12, 2026; exodus 0.1.3 check and audit on copies of them
