free_tool
Is your container Cloud Run ready?
A Dockerfile that builds fine on your laptop can still leak secrets, run as root, or drop requests on scale-down in production. Paste yours and get a graded report tuned for Google Cloud Run, with the specific change to make for each gap.
Runs entirely in your browser. Nothing is uploaded, sent to a server, or stored.
Cloud Run readiness
41/100
3 to fix · 5 warnings · 4 passed · 2 notes · 1 stage
Grade F, score 41 out of 100, 3 to fix, 5 warnings, 4 passed.No secrets baked into the image
criticalline 4Likely baked secret(s): API_KEY. Image layers are world-readable to anyone who can pull the image, so a baked secret is a published secret, and it stays in the layer history even if a later layer overwrites it.
# Inject at deploy time, never in the image:
# gcloud run deploy --set-secrets=API_KEY=my-secret:latest
# (store the value in Secret Manager)Runs as a non-root user
highNo non-root USER set, and base "node" defaults to root. A container escape or compromised process then has root. Create an unprivileged user and switch to it after the last step that needs root.
RUN addgroup --system app && adduser --system --ingroup app app
USER appMulti-stage build keeps build tooling out of the image
mediumThere's a build/compile step but a single stage, so the build toolchain (compilers, node_modules, dev deps) ships in the final image. That bloats the image and widens the attack surface. Build in one stage, COPY only the artifacts into a clean runtime stage.
FROM node:20-slim AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modulesBase image is pinned to a version
mediumline 1Base "node:20" is tagged but not pinned by digest. A tag can be re-pushed; pin "@sha256:..." for byte-for-byte reproducible builds.
Minimal runtime base image
mediumline 1"node:20" is the full base. It ships more packages than you need, which means a larger CVE/attack surface and slower image builds and pushes. (Cloud Run streams image layers, so size barely affects cold start, but a leaner runtime is still worth it.) Switch the runtime stage to a slim, alpine, or distroless variant.
FROM node:<version>-slimENTRYPOINT/CMD uses exec form (clean SIGTERM)
mediumline 13Shell form (e.g. CMD node server.js) runs your process under /bin/sh -c, and a plain shell does not forward SIGTERM to its child. When Cloud Run scales an instance down it sends SIGTERM, then SIGKILLs after the grace period, so your process never sees the signal and in-flight requests are dropped instead of draining. Use JSON (exec) form.
CMD ["node", "server.js"]Broad COPY/ADD is paired with a .dockerignore
lowline 8Copying the whole build context (COPY/ADD . ) without a .dockerignore that excludes .git, node_modules, .env and local secrets can ship credentials and bloat into the image. (This auditor can't see your .dockerignore, so make sure it exists.)
# .dockerignore
.git
node_modules
.env
*.log
Dockerfile
.dockerignoreOS package installs stay lean
lowline 9apt install pulls recommended packages you probably don't need and leaves /var/lib/apt/lists in the layer. Both inflate the image. Add --no-install-recommends and rm the lists in the same RUN.
RUN apt-get update && apt-get install -y --no-install-recommends \
<pkgs> \
&& rm -rf /var/lib/apt/lists/*Defines a start command
A CMD/ENTRYPOINT is set in the final stage, so the container knows what to run.
Uses COPY, not ADD, for local files
No ADD instructions, so COPY is the predictable choice.
No reliance on a writable VOLUME
No VOLUME. Good, since Cloud Run's filesystem is ephemeral and in-memory anyway.
Sets an explicit WORKDIR
WORKDIR is set, so relative paths and the runtime working directory are unambiguous.
Listens on 0.0.0.0:$PORT
line 12Cloud Run injects the PORT env var (default 8080) and routes traffic to it; EXPOSE is documentation only and is ignored for routing. You EXPOSE 3000, which is fine as a hint, but make sure your server reads process.env.PORT and binds 0.0.0.0, not a hardcoded 3000 on localhost.
Health checks belong on the service, not the Dockerfile
No Dockerfile HEALTHCHECK needed, since Cloud Run ignores it anyway. Configure startup and liveness probes on the service to control when an instance receives traffic.
A clean Dockerfile is the floor, not the ceiling. The cold starts, the autoscaling, the concurrency, the memory limits and the secrets wiring are where Cloud Run bills and breaks. That's the kind of review I do.
Get your deploy production-ready: book a callStatic analysis of the Dockerfile text only. It can't see your app code, your .dockerignore, or your Cloud Run service config, so a clean grade means the Dockerfile is sound, not that the deploy is. It runs entirely in your browser and uploads nothing.
why_it_matters
Cloud Run changes what a good container looks like
Cloud Run injects a $PORT your server must bind on 0.0.0.0, ignores your HEALTHCHECK in favor of service probes, gives you an in-memory filesystem with no persistent disk, and sends a SIGTERM when it scales an instance down. Miss the signal and you drop in-flight requests; miss the port and the deploy never goes healthy.
Cloud Run streams image layers, so a leaner base is about a smaller attack surface and faster builds, not cold-start latency. This auditor encodes those platform rules, plus the usual hardening (non-root, no baked secrets, pinned base, multi-stage), so the obvious mistakes get caught before they page you.
faq
Questions & answers
- What does the Dockerfile & Cloud Run Auditor check?
- It parses your Dockerfile and grades it across security (non-root user, no baked-in secrets, COPY over ADD), image health (pinned and slim base, multi-stage builds, lean package installs), and Cloud Run fit (exec-form CMD for clean SIGTERM, a start command, no reliance on a writable VOLUME). Each finding comes with why it matters and the exact line to change.
- What port should my container listen on for Cloud Run?
- Cloud Run injects a PORT environment variable, 8080 by default, and routes traffic to it. Your server must read process.env.PORT (or the language equivalent) and bind 0.0.0.0, not 127.0.0.1. The EXPOSE instruction is documentation only on Cloud Run and is ignored for routing, so hardcoding a port other than what your app binds is a common reason a deploy never goes healthy.
- Why does the exec form of CMD matter on Cloud Run?
- When Cloud Run scales an instance down it sends SIGTERM, then SIGKILLs after a grace period. Shell-form CMD (CMD node server.js) runs your process under /bin/sh -c, which often doesn't forward that SIGTERM, so in-flight requests get dropped instead of draining. JSON exec form (CMD ["node", "server.js"]) makes your process PID 1 so it receives the signal and can shut down gracefully.
- Does Cloud Run use my Dockerfile HEALTHCHECK?
- No. Cloud Run ignores the Dockerfile HEALTHCHECK instruction. Configure startup and liveness probes on the Cloud Run service instead; those are what actually gate traffic to a new instance and trigger restarts. The auditor flags this as a note rather than a failure.
- Why does image size matter more on Cloud Run than elsewhere?
- Cloud Run pulls your image on a cold start, so a fat image is latency for your first user after a scale-to-zero, not just a registry bill. That's why the auditor pushes a slim, alpine, or distroless runtime base and a multi-stage build that keeps compilers and dev dependencies out of the final image.
- Is my Dockerfile uploaded or stored anywhere?
- No. The analysis runs entirely in your browser. Nothing you paste is sent to a server or stored, so it's safe to check a Dockerfile that references internal images or build args.
Want the rest of the deploy looked at?
The Dockerfile is the floor. I'll review the cold starts, concurrency, memory limits, autoscaling and secrets wiring that actually decide what Cloud Run costs and whether it stays up. Book a call, or leave your email.
Prefer proof first? See how this plays out in real case studies →