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.
What happens automatically
Section titled “What happens automatically”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
EXPOSEline, 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.
Requirements
Section titled “Requirements”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=8000EXPOSE 8000CMD ["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.
Environment variables
Section titled “Environment variables”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_URLENV DATABASE_URL=$DATABASE_URLWriting one that builds fast
Section titled “Writing one that builds fast”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 changesCOPY package*.json ./RUN npm ci
# Source: changes on every commit, so it goes lastCOPY . .RUN npm run buildThe 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.gitdist.envA 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 buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:22-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY --from=build /app/dist ./distCMD ["node", "dist/main.js"]Compose files
Section titled “Compose files”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.
Switching between build modes
Section titled “Switching between build modes”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.