Skip to content

Using a Dockerfile

If your repository already has a Dockerfile, ZevCloud uses it as-is. You’ve described your build; we don’t second-guess it.

When you create a service from a repository, we look for a Dockerfile in your Root Directory. If one is there:

  • The Build Pack is set to Dockerfile for you.
  • The Port is filled in from the file’s EXPOSE line, when it has one.
  • Your install and build commands are ignored — your Dockerfile is the whole build.

dockerfile (lowercase) and Containerfile are recognised too. We only look in the Root Directory, not in subfolders.

Bind to $PORT. ZevCloud sets a PORT environment variable on every service. Reading it is the most reliable option, because then the port can never drift out of sync with your service settings:

ENV PORT=8000
EXPOSE 8000
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"]

If you’d rather hard-code a port, make sure EXPOSE, your CMD, and the Port in Service Settings all agree. A mismatch produces a Bad Gateway on a container that’s running perfectly.

Listen on 0.0.0.0, not 127.0.0.1. An app bound to localhost inside a container is unreachable from outside it. This is the second most common cause of a healthy container that serves nothing.

Build for linux/amd64. If you build and test locally on an Apple Silicon Mac, use docker build --platform linux/amd64 to catch architecture problems before deploying rather than after.

Variables you set in the Variables tab are available as build arguments and at runtime. To use one during the build, declare it:

ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL

Docker reuses layers until one changes, and every layer after that point is rebuilt. So the order of your instructions decides how much work each deploy does.

Copy your dependency manifest first, install, and only then copy your source. That way a code change reuses the install layer:

# Dependencies: only re-runs when the lockfile changes
COPY package*.json ./
RUN npm ci
# Source: changes on every commit, so it goes last
COPY . .
RUN npm run build

The mistake to avoid is COPY . . before the install step, which makes every commit reinstall every dependency.

Add a .dockerignore so node_modules, .git, and build output aren’t shipped into the build:

node_modules
.git
dist
.env

A multi-stage build keeps compilers and dev dependencies out of the final image, which makes deploys faster and the running container smaller:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/main.js"]

ZevCloud deploys one container per service, so docker-compose.yml isn’t used as a build input. If your compose file describes several containers, create one ZevCloud service per container and point each at the folder holding its Dockerfile.

For the things compose usually provides alongside your app:

  • Databases — create a managed database and connect with the connection string it gives you.
  • Object storage — use storage buckets.
  • Shared networking — services in the same project reach each other over the internal network.

Service Settings → Build & Deploy → Build Pack lets you switch at any time. Changing to Dockerfile requires a Dockerfile in your Root Directory; if there isn’t one, the change is rejected rather than silently falling back, so you don’t end up with a build that quietly ignores the setting.

Either way the change applies on the next deploy.