All posts

Buildpacks or a Dockerfile: how to get your code into a Cloud Run container

One command builds your Cloud Run container with Google's buildpacks or your own Dockerfile. What each does, what it costs, who patches the OS, what breaks, and the commands for both.

Cloud Run builds your container with one command, gcloud run deploy --source .: if the folder has a Dockerfile at its root, Cloud Build runs it; if not, Google's buildpacks detect the language and build the image. Buildpacks fit a plain Node.js, Python, Go, Java, Ruby, PHP or .NET app, and Google can patch their base image; a Dockerfile fits everything else.

In short

  • Cloud Build gives 2,500 free build-minutes a month, then $0.006 a minute; building on your own machine costs no build-minutes.
  • The same Express app was 390 MB with buildpacks, 239 MB on node:24-slim and 181 MB on node:24-alpine; on Cloud Run, image size does not affect startup.
  • Artifact Registry stores the first 0.5 GiB free, then $0.10 per GiB a month, and source deploys never delete old images.
  • Node.js 20 is decommissioned on October 30, 2026; pin the runtime version, because an open range like >=16 built with Node.js 26 in our test.

Commands were checked against Google Cloud SDK 587.0.0 on October 11, 2026; prices are Google's list prices of the same day.

What one command does

Cloud Run does not run your code directly. It runs a container image: a packed file system with your code, its packages, a language runtime and a small Linux, plus the command that starts it (what Cloud Run is covers the rest of the model). From a folder with your code, this builds and deploys a service called my-app:

gcloud run deploy my-app --source . --region europe-west1

Nothing is built on your machine. gcloud uploads the folder, leaving out what .gcloudignore lists; when there is no .gcloudignore but there is a .gitignore, gcloud generates an ignore file from it. Cloud Build, Google's build service, then checks for a Dockerfile. With one, it runs a normal Docker build. Without one, it runs Google's buildpacks. The finished image goes to Artifact Registry, Google's image store, in a repository called cloud-run-source-deploy in the same region, created on the first deploy if it is missing. Cloud Run then starts a new revision from that image and, once it passes its startup check, sends it 100% of the traffic.

A diagram: your folder goes to Cloud Build, which uses Docker when there is a Dockerfile and buildpacks when there is not; the image lands in Artifact Registry and becomes a new Cloud Run revision
What gcloud run deploy --source . does. The Dockerfile at the root decides which builder runs; everything else is the same.

Google calls this a convenience feature and says plainly that it "does not allow full customization of the build". For full control you build the image yourself, with Cloud Build or Docker, and deploy it by its address with --image.

The first deploy from source in a new project needs two APIs and some roles. You, the deployer, need Cloud Run Source Developer (roles/run.sourceDeveloper), Service Usage Consumer and Service Account User on the service's identity. The build itself runs as the Compute Engine default service account, which needs the Cloud Run Builder role. In organizations created on or after May 3, 2024, Google no longer grants that account the broad Editor role automatically (the guide on setting up a Google Cloud project covers that baseline), so on a fresh project this grant is often the missing piece:

gcloud projects add-iam-policy-binding PROJECT_ID \
  --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
  --role=roles/run.builder

Google recommends going further and giving the build its own service account with only that role, through --build-service-account projects/PROJECT_ID/serviceAccounts/EMAIL on the deploy.

What buildpacks do

Buildpacks are an open standard, Cloud Native Buildpacks, and Google publishes its own set of them as an open source project. A buildpack handles one piece of the job, such as installing Node.js or running npm ci. A builder is an image that bundles many buildpacks; it looks at your folder, picks the ones that apply, and runs them in order. gcloud run deploy --source always uses gcr.io/buildpacks/builder:latest, which today is the google-24 builder on Ubuntu 24.

The google-24 builder supports Python 3.13 and 3.14, Node.js 22, 24 and 26, Go 1.x, Java 17, 21 and 25, Ruby 3.2 to 4.0, PHP 8.2 to 8.5, .NET 8 and 10, and an "OS only" runtime for a binary you compiled yourself. Rust, Elixir and anything else outside that list need a Dockerfile. The guide to Node, Python, Rails and Go on Cloud Run has the traps of each stack.

You can run the same builder on your own machine with pack, the buildpacks command line tool, and Docker. This is the real output of pack on Google's own Node.js sample, an Express app of a few lines:

A terminal showing pack build detecting three Node.js buildpacks, installing Node.js v26.7.0, installing 57 npm packages and setting the default process to web
pack 0.40.9 with the google-24 builder on Google's sample-node, run by the Exodus team on Oct 11, 2026. The sample asks for node >=16 and gets the newest major, 26.

Three of the builder's six buildpacks took part: the Node.js runtime, npm, and one that labels the image. The runtime buildpack read package.json, found "engines": {"node": ">=16"}, and installed Node.js 26.7.0, the newest version the range allows. Google's Node.js page warns about exactly this: avoid "greater than" ranges in engines.node, because the build takes the highest version that matches, and on the next build that may be a new major version you never tested.

The rest of the build did what a careful Dockerfile would do: npm ci with NODE_ENV=production, so only production packages; a start command taken from scripts.start; a non-root user (uid 33); PORT=8080 set in the image. pack inspect on the result reports the run image gcr.io/buildpacks/google-24/run and Rebasable: true, which matters later in this article.

Telling buildpacks what you want

Buildpacks read the files your language already uses, so most settings live there.

  • Node.js. Pin the version in package.json with "engines": {"node": "24.x.x"}. The package manager follows the lockfile: yarn.lock, then pnpm-lock.yaml, then bun.lock, then package-lock.json; with no lockfile, npm. If package.json has a build script, it runs; a gcp-build script replaces it, and "gcp-build": "" runs nothing. When custom build steps are set, dev dependencies are installed for them first. Without a Procfile, the app starts through npm start.
  • Python. Put the version in .python-version. Python 3.14 and later install requirements.txt with uv, 3.13 and earlier with pip; if both requirements.txt and pyproject.toml exist, requirements.txt wins. On a source deploy with Python 3.13 or later, the start command is guessed from requirements.txt: with fastapi and uvicorn it is uvicorn main:app --host 0.0.0.0 --port 8080, and with no web server listed it falls back to gunicorn -b :8080 main:app. That fallback expects a file main.py with an object called app; if yours is server.py, the deploy fails at startup.
  • Any language. A file called Procfile at the root sets the start command and overrides the guess: web: gunicorn -b :$PORT server:app. Google recommends one for Node.js too, because it starts your app directly instead of through npm.

For anything else, there are build environment variables. They exist only while the image is built and are passed with the deploy:

gcloud run deploy my-app --source . --region europe-west1 \
  --set-build-env-vars GOOGLE_ENTRYPOINT="node server.js",GOOGLE_NODEJS_VERSION=24.x.x

--update-build-env-vars adds or changes some, --remove-build-env-vars and --clear-build-env-vars drop them. The limit is 100 variables and 64 KiB. They are stored with the service, so they are no place for a token: Google sends secrets to Secret Manager. The full list of GOOGLE_* variables is on Google's buildpacks configuration page; the ones you will meet first are GOOGLE_ENTRYPOINT (the start command), GOOGLE_RUNTIME_VERSION, and GOOGLE_BUILDABLE (which Go package or Java module to build).

What a Dockerfile gives you

A Dockerfile is the other answer. You choose the base image, install exactly what you need, and decide what goes into the final image. Cloud Run accepts any image built by any tool, as long as it keeps to the container contract: Linux x86_64 binaries, and a web server listening on 0.0.0.0 at the port in the PORT variable, 8080 by default.

The same Express app, built with a Dockerfile:

FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "web.js"]

Next to it, a .dockerignore keeps node_modules, .git and local .env files out of the build:

node_modules
.git
.env

Copying the lockfiles before the code pays off on every build: Docker reuses the npm ci layer until the dependencies change, so a code change rebuilds in seconds. On our build box a rebuild of this image after a one-line change took under a second.

Reach for a Dockerfile when:

  • the language is not on the buildpacks list (Rust, Elixir, Deno, a C++ server);
  • the app needs system packages you would install with apt, such as the libraries of a PDF renderer or a headless browser; buildpacks can be extended for that, but a Dockerfile is the simpler place;
  • the build is unusual: a monorepo where one service depends on another package, a frontend built in the same image, generated code;
  • you want the same image to run on your laptop, in CI and on Cloud Run, byte for byte;
  • you already have one. If the app ran on Fly, Render, Railway or a server with Docker, its Dockerfile usually runs on Cloud Run as it is, once it listens on PORT and is built for linux/amd64.

For compiled languages a multi-stage Dockerfile is the norm: the first stage has the compiler, the second only the finished binary, so the tools never ship. Docker's own example builds a Go program in golang and copies the binary into an empty scratch image.

To deploy a Dockerfile you can still use gcloud run deploy --source . and let Cloud Build run it. Or build it yourself and push the image:

gcloud artifacts repositories create my-repo \
  --repository-format=docker --location=europe-west1
gcloud auth configure-docker europe-west1-docker.pkg.dev
docker build --platform linux/amd64 \
  -t europe-west1-docker.pkg.dev/PROJECT_ID/my-repo/my-app:v1 .
docker push europe-west1-docker.pkg.dev/PROJECT_ID/my-repo/my-app:v1
gcloud run deploy my-app --region europe-west1 \
  --image europe-west1-docker.pkg.dev/PROJECT_ID/my-repo/my-app:v1

gcloud builds submit --tag IMAGE_URL does the build and push on Cloud Build instead of your machine, and gcloud builds submit --pack image=IMAGE_URL does the same with buildpacks, for when you want a buildpacks image under your own name and repository.

Image size, and why it matters less than you think

We built the same Express app four ways and measured the file system of each image:

A bar chart of image size: buildpacks 390 MB, a Dockerfile on node:24-slim 239 MB, on node:24-alpine 181 MB, and the app alone on scratch 2.4 MB
The same Express app, measured by the Exodus team on Oct 11, 2026. Compressed, the four are 126, 81, 63 and 0.6 MB.

The buildpacks image is the largest: the Ubuntu 24 run image plus Node.js 26. The Debian slim image is about 60% of it, Alpine less. The last bar is the app with its node_modules alone, which is what an image for automatic base image updates contains.

On Cloud Run, size costs less than on most platforms. Google's tips page says that because of Cloud Run's image streaming, "the size of your container image does not affect container startup times or request processing time", and the image does not count against the instance's memory. Cold starts measured shows what does move startup. What size does cost is storage, a little, and attack surface: every package in the image is one more thing a scanner flags and somebody has to patch.

Who patches the operating system

An image is frozen at build time. If OpenSSL in it gets a security fix next month, your running service keeps the old one until someone rebuilds and redeploys. Google's source deploy page states it directly: "By default, security fixes are only applied when the Cloud Run service is deployed."

Services built from source on Google's base images can opt in to automatic base image updates. The image is then split in two: your code and packages on top, and Google's base image (operating system plus language runtime) underneath. When Google patches the base, it swaps the lower part under your running service: no rebuild, no redeploy, no new revision, no downtime. Buildpacks make this possible because their images are built in layers that can be rebased, which is what Rebasable: true meant above.

Two stacks of three layers. With buildpacks and automatic updates, Google patches the runtime and the operating system layers; with your own Dockerfile, you patch all three by rebuilding
Automatic base image updates work only on Google's buildpacks base images. With your own Dockerfile, every fix waits for your next build.

To turn it on, name the base image on the deploy:

gcloud run deploy my-app --source . --region europe-west1 \
  --base-image nodejs24 --automatic-updates

Google's pages disagree on whether this is already the default: the runtimes page says automatic updates are on by default for source deploys, the source deploy page says fixes arrive only on deploy. Do not rely on either; pass both flags and check the service:

gcloud run services describe my-app --region europe-west1 --format=yaml \
  | grep -E 'image:|runtimeClassName|base-images'

A service with automatic updates shows runtimeClassName: run.googleapis.com/linux-base-image-update and a run.googleapis.com/base-images annotation. A service without them shows only its image: line.

There are limits. It works only with Google's base images, never your own. Compiled languages are not recompiled, so a fix in Go's or Java's standard library still needs a build. You can get the same updates with a Dockerfile, but only by Google's recipe: a multi-stage build that copies the app onto scratch, deployed with --image APP_IMAGE --base-image nodejs24; that app-only image no longer runs on your laptop without reassembling it.

Base images also have an end date. Each runtime has a deprecation date and a decommission date; Google notifies you 90 days before deprecation, and after decommission you cannot deploy with that runtime and "workloads that continue to use a decommissioned runtime may be disabled". Node.js 20 is decommissioned on October 30, 2026, Python 3.10 on April 4, 2027, .NET 8 on May 10, 2027. Upgrading in time is your job. Images you build yourself are outside this policy entirely: nobody disables them, and nobody patches them either.

Exodus team: the staged Exodus site itself is the second kind. Its Cloud Run service runs an image built from a five-line Dockerfile on an Alpine nginx image, built on our own machine and deployed by digest; its project has never enabled Cloud Build, and the service has no base image set. For a static page with one dependency that works, and the patching is ours.

What it costs

Both ways cost the same at run time: Cloud Run bills the instance, not how its image was made. The build side has two small bills, with prices read on October 11, 2026.

Cloud Build charges per build-minute. Each billing account gets 2,500 free build-minutes a month on the default e2-standard-2 machine (2 vCPU, 8 GB), which Google labels a promotional tier that may change; after that it is $0.006 a minute, billed by the second, and time in the queue is free. That covers 833 builds of three minutes a month. A team that deploys 30 times a day with three-minute builds uses 2,700 minutes and pays $1.20; at five minutes a build it is 4,500 minutes and $12.00. Building on your own machine or in your CI and pushing the image costs no build-minutes at all.

Artifact Registry charges for stored images: the first 0.5 GiB per billing account is free, then $0.10 per GiB a month. Every source deploy adds an image to cloud-run-source-deploy, and nothing removes them. Keeping 30 full copies of the buildpacks image above would be at most 3.5 GiB, $0.30 a month. Small, but it grows with every deploy. A cleanup policy keeps it bounded; save this as policy.json:

[
  {"name": "delete-old", "action": {"type": "Delete"}, "condition": {"tagState": "any", "olderThan": "30d"}},
  {"name": "keep-recent", "action": {"type": "Keep"}, "mostRecentVersions": {"keepCount": 10}}
]

Then try it without deleting anything, and apply it once the dry run looks right:

gcloud artifacts repositories set-cleanup-policies cloud-run-source-deploy \
  --location=europe-west1 --policy=policy.json --dry-run

The keep rule matters: a rollback to an older revision needs that revision's image, so keep at least as many as you would ever roll back to. Policies run in the background and take effect within about a day.

What breaks

  • A version range picks a new major. "node": ">=16" built with Node.js 26 in our test. Pin 24.x.x, or .python-version, or GOOGLE_RUNTIME_VERSION.
  • The wrong start command. Python without a web server in requirements.txt starts gunicorn -b :8080 main:app; Node.js runs npm start. A Procfile or GOOGLE_ENTRYPOINT ends the guessing.
  • Browser caching of static files. Buildpacks set every file's modification time to January 1, 1980, for reproducible builds. If your framework derives ETag or Last-Modified from it, browsers may keep stale files; Google suggests turning those headers off.
  • An image built on a Mac with Apple silicon. Docker builds arm64 there by default, and Cloud Run runs only linux/amd64. Add --platform linux/amd64, or let Cloud Build build it.
  • Uploading too much, or too little. Without .gcloudignore, the upload follows .gitignore. A 2 GB data/ folder that git does not ignore is uploaded on every deploy; a generated file that git does ignore is missing from the build.
  • A missing role. A build that fails with a permission error on a new project is usually roles/run.builder missing on the build's service account.
  • Secrets in the build. Build environment variables are stored with the service's configuration, and Google says not to use them for secrets; a value baked into the image during a Docker build is no safer. Read secrets at run time from Secret Manager.
  • A Google credentials file in the image. If your Dockerfile runs gcloud during the build, keep ~/.config/gcloud/gce out of the image; Google warns it breaks authentication on Cloud Run.
  • A runtime past its date. A service on a decommissioned runtime cannot be redeployed until you move it to a newer one, which is a bad thing to learn during an incident.

How to choose

Start with buildpacks when your app is Node.js, Python, Go, Java, Ruby, PHP or .NET, builds with that language's normal tools, and has no Dockerfile yet. Pin the runtime version, add a Procfile, turn on automatic updates, and you have an image you do not maintain. This is the shortest way from a Heroku-style app to Cloud Run, because buildpacks read the same package.json, requirements.txt or Gemfile Heroku did; the Heroku to Cloud Run guide covers the rest of that move.

Write or keep a Dockerfile when the language is not supported, the app needs system packages, the build is unusual, or you want one image everywhere. Then own its patching: rebuild on a schedule, not only when the code changes, and scan the image.

Moving from source deploys to your own pipeline later is not a rewrite. gcloud builds submit --pack builds the same buildpacks image under a name you choose, and a Dockerfile can be added at any time; the next --source deploy starts using it. The guide to deploying from GitHub without keys shows the pipeline that replaces your laptop.

This is also the first task in an Exodus plan. When the report finds a supported language and no system dependencies, the checklist says to build from source; when the repository already has a working Dockerfile, it says to keep it.

The first four tasks of an Exodus checklist for a Node.js bot moving from Heroku; task 1 reads: Container, a build from source, gcloud run deploy --source builds it with buildpacks, Node needs no Dockerfile
An Exodus report's checklist (prototype, sample data). The container is task 1, and for a Node.js app it is a build from source.

Checklist

  1. Decide: supported language and a plain build, buildpacks; otherwise a Dockerfile.
  2. Grant roles/run.builder to the build's service account, or give the build its own account with --build-service-account.
  3. Pin the runtime version in the language's own file; no open ranges.
  4. Set the start command with a Procfile or GOOGLE_ENTRYPOINT, and make the app listen on PORT.
  5. Write a .gcloudignore (and a .dockerignore with a Dockerfile) so the upload holds only what the build needs.
  6. Deploy with --base-image and --automatic-updates on buildpacks, and check runtimeClassName in the service's YAML.
  7. With a Dockerfile, build for linux/amd64, run as a non-root user, and rebuild on a schedule for patches.
  8. Put a cleanup policy on the image repository that keeps enough images to roll back.
  9. Note the decommission date of your runtime and upgrade before it.

FAQ

Does Cloud Run need a Dockerfile?

No. Without a Dockerfile at the root of the folder, a source deploy builds the image with Google's buildpacks, which support Node.js, Python, Go, Java, Ruby, PHP and .NET, plus an OS-only runtime for a binary you compiled yourself. Rust, Elixir and other languages need a Dockerfile.

Does a bigger image make Cloud Run start slower?

Google's tips page says that, because of image streaming, the size of the container image does not affect startup or request processing time, and the image does not count against the instance's memory. Size still costs a little storage and adds packages a scanner flags.

Who patches the operating system in a Cloud Run container?

By default nobody until you deploy again: an image is frozen at build time. Services built from source on Google's base images can turn on automatic base image updates, and Google then swaps a patched base under the running service. An image from your own Dockerfile waits for your next build.

How much does building from source cost?

Cloud Build gives each billing account 2,500 free build-minutes a month on the default machine, then $0.006 a minute. A team deploying 30 times a day with three-minute builds pays $1.20 a month. Artifact Registry adds $0.10 per GiB a month above the first free 0.5 GiB.

Sources

26
  1. docs.cloud.google.comDeploy services from source code, Google
  2. docs.cloud.google.comBuild sources to containers, Google
  3. docs.cloud.google.comGoogle Cloud's buildpacks, overview
  4. docs.cloud.google.comBuildpacks builders: google-24, google-22, v1 and their runtimes, Google
  5. docs.cloud.google.comBuildpacks base images and their security patches, Google
  6. docs.cloud.google.comSupported language runtimes and base images, Google
  7. docs.cloud.google.comCloud Run runtime lifecycle, Google
  8. docs.cloud.google.comConfigure automatic base image updates, Google
  9. docs.cloud.google.comBuildpacks environment variables for Cloud Run services, Google
  10. docs.cloud.google.comSet build environment variables
  11. docs.cloud.google.comSet build service account
  12. docs.cloud.google.comAbout Procfiles, Google Cloud's buildpacks
  13. docs.cloud.google.comBuild a Node.js application with buildpacks, Google
  14. docs.cloud.google.comBuild a Python application with buildpacks, Google
  15. docs.cloud.google.comContainer runtime contract, Google
  16. docs.cloud.google.comGeneral development tips for Cloud Run, Google
  17. cloud.google.comCloud Build pricing, Google
  18. cloud.google.comArtifact Registry pricing, Google
  19. docs.cloud.google.comArtifact Registry cleanup policies, Google
  20. docs.cloud.google.comBest practices for speeding up builds, Google Cloud Build
  21. docs.cloud.google.comgcloud run deploy, gcloud builds submit and gcloud topic gcloudignore --help, Google Cloud SDK 587.0.0
  22. docs.docker.comMulti-stage builds, Docker
  23. docs.docker.comBuild context and .dockerignore, Docker
  24. buildpacks.ioWhat happens during rebase, Cloud Native Buildpacks
  25. Exodus team: Google's sample-node built with pack 0.40.9 and the google-24 builder, and with three Dockerfiles, sizes measured on Oct 11, 2026
  26. Exodus team: the staged Exodus site's Cloud Run service, built from a Dockerfile on our own machine, checked with gcloud on Oct 11, 2026

Exodus moves your app to Cloud Run.

A free web cabinet and an open-source CLI. With your coding agent, they audit your app, show what it costs today and on Google Cloud Run, and move it there step by step.

Sign up for freeSee a sample report
  1. Install the CLIOne line in your terminal, once per machine.
  2. Give the prompt to your agentSign in with Google and copy one prompt. Your agent audits the app on your machine.
  3. Read the report, then moveThe bill today and on Cloud Run, what to change and in what order. The agent does the moving.

Read next

Consult an expert

For a move that is hard to make alone: many services, a large database, no room for downtime. An engineer who has moved production apps to Cloud Run reads what you send and replies by email.

Optional.

What the app does and what it runs on today.

Optional. Anything that would change the plan: compliance, regions, a contract that ends.

Your email goes to our engineers and nowhere else.