Console

Dockerfile templates

Copy-paste starting points for the four common stacks.

How the build works

The pipeline is four steps:

  1. We clone your repo — public URL, or via GitHub App install for private.
  2. Cloud Build runs docker build . at the repo root. Whatever your Dockerfile says is what gets built.
  3. The resulting image is pushed to Artifact Registry, tagged with your deployment ID.
  4. Cloud Run pulls that image, injects your env vars at runtime, and starts the container.

So your repo needs a file named Dockerfile at the root. Pick a template below and adjust the entrypoint to your app.

Three rules your Dockerfile must follow

  1. Listen on $PORT. Cloud Run injects the port number as an environment variable at container start. Your app has to bind to it, not to a hardcoded 3000 / 8000 / 80. The templates below default to 8080, which Cloud Run honors out of the box.
  2. Don’t put secrets in ENV. Two things go wrong if you do. First, ENV lines get baked into the image layers — anyone who can pull the image can read them back with docker history. Second, for any key we inject at deploy time — DATABASE_URL, UNINC_JWT_SECRET, UNINC_PROXY_URL, plus whatever you set in the Env tab — our runtime value silently overrides yours. So a baked secret leaks into the image and the app never sees it. Use ENV only for non-secret constants like NODE_ENV=production.
  3. Build-time env vars don’t come from us. Cloud Run injects env at container start, after the image is already built. Frameworks that bake env into bundles at build time — NEXT_PUBLIC_* in Next.js, VITE_* in Vite, REACT_APP_*in CRA — won’t pick up values you set in our Env tab. If your client bundle needs a config value, commit it to your repo or pass it via ARG + ENV in your Dockerfile.

Templates

Four starting points for common stacks. Each follows the three rules above. Click copy, paste into a file named Dockerfile at your repo root, push.

Next.js

App Router or Pages — paste this and your repo deploys with no next.config changes. Uses next start from a full node_modules, the same command npm start runs locally.

# Next.js (App Router or Pages) — works with any next.config
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
COPY --from=build /app .
EXPOSE 8080
CMD ["npx", "next", "start", "-p", "8080"]

Node (generic)

Works for Express, Fastify, Hono, NestJS, or any Node server. Adjust the CMD entrypoint to your build output.

# Generic Node / Express / Fastify / Hono / NestJS
FROM node:20-alpine AS build
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi
COPY . .
# Compile if a build script is present (TypeScript, bundlers); skip for plain JS.
RUN npm run build --if-present

FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
COPY --from=build /app .
EXPOSE 8080
# Adjust to your build output (e.g. "dist/index.js", "build/server.js", "src/server.js").
CMD ["node", "dist/index.js"]

Python (FastAPI / Flask)

Slim base image, uvicorn serving FastAPI. Swap the CMD for gunicorn + Flask or any other WSGI/ASGI runner.

# Python (FastAPI / Flask)
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PORT=8080
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
# `exec` so uvicorn becomes PID 1 — Cloud Run's SIGTERM reaches it directly
# and graceful shutdown actually works. Without it, sh swallows the signal
# and Cloud Run hard-kills the container after 10s.
# FastAPI via uvicorn — replace "app.main:app" with your entrypoint.
# For Flask: CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:$PORT app.main:app"]
CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port $PORT"]

Go

Static binary on distroless — tiny image, no shell. Build output is your module's main package.

# Go (module build, static binary)
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Builds the root main package. For cmd/server-style layouts, change `.` → `./cmd/server`.
# -ldflags="-s -w" strips symbols + DWARF for a smaller binary.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app .

FROM gcr.io/distroless/static-debian12
ENV PORT=8080
COPY --from=build /out/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]

Not about the proxy

These templates are for your application— the image Cloud Run pulls and starts. The Unincorporated proxy, chain-engine, and observer live in their own containers on VMs we provision for you; you don’t write Dockerfiles for any of that. Self-hosting the proxy stack instead? See server/docker/ in the open-source repo.