Render Tutorials
Advanced render.yaml Blueprint patterns

Docker and image runtimes

⏱ 8 min

Native runtimes (node, python, go, …) cover most apps. When you need something they don’t - a custom system dep, a binary that’s a pain to install on every build, a single image you ship to several clouds - you reach for one of two container-shaped runtimes:

  • runtime: docker - Render builds your Dockerfile from the repo and runs the resulting image.
  • runtime: image - Render runs a prebuilt image you’ve already published to a container registry.

Same end state, different operational tradeoffs.

Side-by-side: same service, two ways

render.yaml
services:
- type: web
name: api
runtime: docker
plan: starter
dockerfilePath: ./Dockerfile
dockerContext: .
dockerCommand: node dist/index.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString

Render clones the repo, runs docker build from the context, and deploys the resulting image. Every git push triggers a rebuild.

render.yaml
services:
- type: web
name: api
runtime: image
plan: starter
image:
url: ghcr.io/myorg/api:v1.4.2
creds:
fromRegistryCreds:
name: ghcr
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString

Render pulls the published image and runs it. Pushing code does nothing - you ship a new version by publishing a new image and pointing the Blueprint at it.

The wiring (envVars, fromDatabase, disk, scaling, previews) is identical between the two - they’re both just services. What changes is where the image comes from.

runtime: docker - Render builds for you

render.yaml
services:
- type: web
name: api
runtime: docker
plan: starter
dockerfilePath: ./apps/api/Dockerfile
dockerContext: .
dockerCommand: node dist/index.js
envVars:
- key: PORT
value: "10000"
FieldWhat it doesDefault
dockerfilePathPath to the Dockerfile to build, from repo root./Dockerfile
dockerContextBuild context handed to docker buildThe directory containing the Dockerfile
dockerCommandOverride the container’s CMDWhatever the Dockerfile defines

The two you’ll override most often:

  • dockerContext: . - Set it to the repo root when your Dockerfile is in a subdirectory (apps/api/Dockerfile) but needs to COPY files from packages/ next door. Without this override, your build fails with not found on those COPY lines.
  • dockerCommand - Useful when one Dockerfile produces a multi-purpose image (one for web, one for worker). Override it per service in the Blueprint instead of building two images.
render.yaml - one image, two services
services:
- type: web
name: api
runtime: docker
plan: starter
dockerfilePath: ./Dockerfile
dockerCommand: node dist/server.js
- type: worker
name: jobs
runtime: docker
plan: starter
dockerfilePath: ./Dockerfile
dockerCommand: node dist/worker.js

Render still builds the Dockerfile twice (once per service) - but they’re identical builds and Render caches layers across them. The savings come from not maintaining two Dockerfiles.

runtime: image - bring your own image

render.yaml
services:
- type: web
name: api
runtime: image
plan: starter
image:
url: ghcr.io/myorg/api:v1.4.2
creds:
fromRegistryCreds:
name: ghcr

The image block has two parts:

FieldNotes
urlFully-qualified image URL: registry/namespace/image:tag
creds.fromRegistryCredsReference to a workspace-level registry credential. Omit for public images.

Set up a registry credential in the Render Dashboard once (Account → Registry Credentials), then reference it by name. The Blueprint never holds the credential itself.

flowchart LR
  ci["CI builds + pushes image"]
  registry[(GHCR / ECR / Docker Hub)]
  blueprint["render.yaml - image: tag v1.4.2"]
  render["Render pulls + runs"]

  ci --> registry
  blueprint -->|"references tag"| registry
  registry --> render

A common pipeline: CI tags an image with a short Git SHA, the Blueprint pins to that tag, a separate PR bumps the tag when you want to deploy. Promotion = changing one line in render.yaml.

Picking between them

You wantPick
The simplest path. Push code, Render builds.runtime: docker
To pin production to a specific image SHA.runtime: image
To deploy the same image to multiple clouds.runtime: image
Build caching across CI and Render.runtime: image (your CI keeps the cache)
To not maintain a Dockerfile and a CI pipeline.runtime: docker
Different CMD per service from one image.Either, but runtime: docker with dockerCommand is simpler

The split most teams settle on: runtime: docker early, when builds are simple and CI isn’t worth the overhead. Switch to runtime: image once you have a real CI pipeline and want deploys decoupled from pushes.

The immutability rule

This is one of those things that feels like a bug until you try the alternative - silently changing a service’s runtime in place would be an uncomfortable surprise mid-deploy. The same rule applies to type (webworkercron): immutable, change requires a new service.

A small Docker performance tip

If you’re using runtime: docker, lean on layer caching. The classic package.json / package-lock.json pattern skips the npm ci step when only source code changed:

Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["node", "dist/index.js"]

Two-stage build, lockfile copied separately, multi-stage COPY --from=builder. With layer caching, source-only commits skip the npm ci line and rebuild in seconds instead of minutes.

Your team publishes container images from GitHub Actions on every merge to `main`, tagged with the commit SHA. You want production to track the most recent green tag, with rollback as fast as editing one line. Which runtime fits?

What you learned

  • `runtime: docker` builds your Dockerfile on Render's build host. Tune with `dockerfilePath`, `dockerContext`, and `dockerCommand`
  • `runtime: image` pulls a prebuilt image from a registry. Use `image.url` plus `image.creds.fromRegistryCreds` for private registries
  • Both runtimes share the same wiring, scaling, disk, and preview surfaces - they're just services with a different image source
  • `runtime` is immutable. Switching runtimes requires renaming the service so Render provisions a new one and tears the old one down