The shortest useful form is gcloud run deploy my-service --source . --region us-central1, and it will get a container built and a URL back without you writing a Dockerfile.
That is genuinely impressive, and it is also where most teams stop reading, which is how they end up with a service that cold-starts for four seconds, scales to a hundred instances during a crawl, and holds a database connection pool that the database cannot support.
This is a tour of the flags that actually change behaviour in production, and the defaults that quietly do something other than what you expected.
Table of contents
- Source deploy versus image deploy
- Settings carry over between deploys, which is a trap
- Concurrency, CPU and the flags that decide your bill
- Environment variables and secrets
- Traffic, revisions and the safe rollout
- The database connection problem this architecture creates
- How this fits the rest of the stack
- FAQ
Source deploy versus image deploy
There are two ways in, and the choice has consequences beyond convenience.
# From source: buildpacks detect the language, build an image, push it, deploy.
gcloud run deploy my-service --source . --region us-central1
# From a pre-built image: nothing is built, the image is deployed as-is.
gcloud run deploy my-service \
--image us-docker.pkg.dev/my-project/repo/my-image:v1.2.3 \
--region us-central1
Source deploys are excellent for the first week. They are slower, they build on every deploy, and the resulting image is whatever the buildpack decided, which means you cannot reproduce it locally with confidence.
Image deploys are what you want once anything matters. Build once in CI, tag with the commit SHA, deploy the exact bytes you tested. The rule of thumb: if you cannot name the image that is currently serving production, you have a debugging problem waiting to happen.
If a Dockerfile is present in the source directory, the source deploy uses it rather than buildpacks. That is usually the behaviour you want and it is easy to be surprised by if you added a Dockerfile for local development only.
Settings carry over between deploys, which is a trap
This is the single most misunderstood behaviour of the command. Flags you set once persist to subsequent deploys unless you explicitly change them.
So this sequence does what you probably do not want:
# Monday: a one-off experiment.
gcloud run deploy my-service --image img:v1 --max-instances 100 --region us-central1
# Friday: someone else deploys, unaware.
gcloud run deploy my-service --image img:v2 --region us-central1
# max-instances is still 100.
The upside is that routine deploys stay short. The downside is that your service configuration lives in shell history rather than in a file, and nobody can read the current state without asking the API.
The fix is to keep the full configuration in a YAML service definition under version control, or at minimum to run a describe command in CI and diff it. Reading the live configuration is one line:
gcloud run services describe my-service --region us-central1 --format yaml
Concurrency, CPU and the flags that decide your bill
Cloud Run bills for CPU and memory while a container instance is handling requests. Three flags dominate that number.
Concurrency is how many simultaneous requests one instance handles, and the default of 80 is high. For a CPU-bound service that is far too many and every request gets slow; for an IO-bound service it is often fine or even low. Set it deliberately based on what your handler actually does.
CPU allocation decides whether you are billed between requests. The default throttles CPU when no request is in flight, which is cheap and correct for a plain web service. If you have background work continuing after the response, that work will be starved and the bug will be intermittent and horrible to diagnose.
Min instances is the cold-start lever. Zero means you pay nothing when idle and the first request after a quiet period waits for a container to start. One means a warm instance always exists and you pay for it around the clock.
gcloud run deploy my-service \
--image img:v2 \
--region us-central1 \
--concurrency 20 \
--cpu 1 --memory 512Mi \
--min-instances 1 \
--max-instances 10 \
--timeout 30s
Set max-instances on every service. The default ceiling is high enough that a traffic spike, a retry storm or an enthusiastic crawler can scale you into a bill you did not budget for, and the failure mode is financial rather than operational, which means nothing pages you.
Environment variables and secrets
The flags here are easy to get subtly wrong, because two of them look similar and one destroys data.
# Replaces every existing variable. Anything not listed is gone.
--set-env-vars "LOG_LEVEL=info,REGION=eu"
# Adds or updates the named ones, leaves the rest alone. Usually what you want.
--update-env-vars "LOG_LEVEL=debug"
# Secrets come from Secret Manager rather than the command line.
--set-secrets "DATABASE_URL=db-url:latest"
Never pass a credential through set-env-vars. It lands in your shell history, in your CI logs, and in the service definition in plain text. Secret references keep the value out of all three and give you rotation without a redeploy.
Pinning a secret to latest means a rotation takes effect on the next instance start rather than immediately, which is usually the behaviour you want but is worth knowing during an incident.
Traffic, revisions and the safe rollout
Every deploy creates an immutable revision, and by default all traffic moves to it at once. For anything with real users, that is more confidence than most deploys deserve.
# Deploy without taking traffic.
gcloud run deploy my-service --image img:v3 --no-traffic \
--tag candidate --region us-central1
# The tagged revision now has its own URL for smoke tests.
# Then move traffic gradually:
gcloud run services update-traffic my-service \
--to-revisions candidate=10 --region us-central1
# And back out instantly if the error rate moves:
gcloud run services update-traffic my-service --to-latest --region us-central1
The rollback is the important half. Because revisions are immutable, reverting is a traffic change rather than a rebuild, which means it takes seconds and cannot fail on a build error at the worst possible moment.
The database connection problem this architecture creates
Request-scaled containers and traditional connection pools are a bad match, and it is the most common production surprise on this platform.
Each instance holds its own pool. Scale to 50 instances with a pool of 10 and you have asked your database for 500 connections. Most managed database plans do not offer that, so what you get is connection refusals under exactly the load that caused the scaling.
Three things fix it, in order of how much work they are:
- Set max-instances so the worst case is a number your database can survive.
- Shrink the per-instance pool. With request-based concurrency, one or two connections per instance is often correct rather than the framework default of ten.
- Put a connection pooler in front of the database so the fan-in happens somewhere designed for it.
This is worth calculating before launch rather than during. Multiply your max instances by your pool size and compare it against your database connection limit. If the first number is larger, you have a scheduled outage. The database connection limits documentation covers the same arithmetic on RunxBuild, where the ceiling comes with the plan and is visible up front.
How this fits the rest of the stack
Request-scaled deployment is a genuinely good fit for spiky, stateless workloads, and a genuinely awkward one for a service with a warm cache, long-lived connections or steady baseline traffic, where a plan you pay for continuously is both cheaper and simpler to reason about. Working out which one you have is mostly arithmetic, and the RunxBuild hosting calculator is a quick way to see what the steady-state version costs before you optimise around cold starts you may not need to care about.
Useful related references:
- how to run a docker image: The Five Flags, the Four Pitfalls, and the Deploy Path That Actually Works
- What Is an FTP Server, and Should You Run One in 2026?
- Scheduling with Crontab: The Syntax, and Why Your Job Did Not Run
- Services on RunxBuild
FAQ
What is the difference between deploying from source and deploying an image?
A source deploy builds a container for you using buildpacks, or your Dockerfile if one is present. An image deploy ships bytes you built earlier. Source is faster to start with; image deploys are reproducible, faster to deploy, and let you deploy the exact artefact you tested, which is what you want once anything is in production.
Why is my service still using an old setting I did not pass?
Because configuration persists between deploys. Any flag you set once stays set until you explicitly change or clear it, so a flag from an experiment weeks ago is still in effect. Run the describe command to read the live configuration, and keep it in version control rather than in shell history.
How do I stop cold starts?
Set min-instances to at least one so a warm container always exists. You then pay for that instance continuously, which is the trade. Enabling CPU boost helps the start itself, and reducing image size and deferring non-essential initialisation help more than most people expect.
What should I set concurrency to?
It depends on what your handler does. CPU-bound work wants a low number, often between 1 and 10, because instances cannot genuinely parallelise beyond their CPU allocation. IO-bound work that spends most of its time waiting can handle the default of 80 or more. Load test both rather than guessing.
Why does my database run out of connections when traffic spikes?
Every container instance holds its own connection pool, so total connections are instances multiplied by pool size. Fifty instances with a pool of ten asks for five hundred connections, which most database plans will refuse. Cap max-instances, shrink the per-instance pool, or add a connection pooler in front of the database.