A Dockerfile in the repository always wins - run anything as a container, with the port, PORT variable, and image-size rules that make it work.
Deploy with a Dockerfile
A Dockerfile in the repository always wins: detection steps aside and the app builds and runs exactly as written, whatever is inside. This is the escape hatch for unlisted frameworks, unusual toolchains, and anything else that runs in a container.
The contract#
Three things the container must do:
- Listen on the routed port. Traffic is routed to the port setting (default
8080for Dockerfile apps). The same value arrives as thePORTenvironment variable - reading it is the most portable choice. - Bind 0.0.0.0. A server bound to
127.0.0.1is unreachable from outside the container. - Log to stdout and stderr. That is what lands in runtime logs.
Configuration arrives as environment variables at runtime - the image build itself runs without them, so don't bake secrets or config into layers.
An example#
A small multi-stage Node image:
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
EXPOSE 8080
CMD ["node", "dist/server.js"]
The same pattern works for Go (scratch or distroless final stage), Rust, JVM apps, and anything else: build in one stage, run from a minimal one.
Keep the image lean#
Image size is startup time. An app that scales to zero pays its image's boot cost on the first request after idle:
- Multi-stage builds - compilers and dev dependencies stay in the build stage.
- Slim or distroless base images.
- A
.dockerignorethat excludesnode_modules,.git, local caches, and test data from the build context.
Notes#
EXPOSEis documentation - routing uses the port setting, not the EXPOSE line. Keep them matching for your own sanity.- The build command, when set, runs during the image build.
- Scaling settings apply as usual: instances, concurrency, CPU target.
- A green build with an unreachable app is almost always a port mismatch or a
127.0.0.1bind - see When a build fails.