DocsCustom Dockerfile

Custom Dockerfile

If your repo has a Dockerfile, Ruust builds it instead of Nixpacks. You get full control of the image, as long as it listens on $PORT bound to 0.0.0.0.

By default Ruust builds your repo with Nixpacks, auto-detecting the stack. If it finds a Dockerfile at the repo root, it builds that instead and Nixpacks steps aside entirely. This is the escape hatch: your image, your base, your build steps, no detection guessing. Everything else about the Egg (the URL, TLS, env vars, no cold starts) works exactly the same.

When to reach for it

Most apps do not need a Dockerfile. Reach for one when you need a specific base image, a system package Nixpacks will not add for you, a compiled toolchain, or a multi-stage build that keeps the final image small. If you are happy with auto-detection, delete the Dockerfile and Ruust falls back to Nixpacks on the next deploy.

Deploy it

  1. Add a Dockerfile to the root of your repo and commit it.
  2. Push to your Git provider.
  3. In the dashboard, choose New Egg and connect the repo (or select an existing Egg pointed at it).
  4. Pick a size and a region, then lay the Egg. Ruust sees the Dockerfile and builds it instead of Nixpacks.

The port

This is the rule that matters. Ruust injects a PORT environment variable at run time and expects the final image to listen on it, bound to 0.0.0.0 so traffic from outside the container reaches your process. Do not hard-code a port and do not bind to localhost or 127.0.0.1. Binding wrong is the top reason an Egg builds cleanly but never hatches.

dockerfile
# Multi-stage build: compile in one stage, ship a slim runtime.
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/package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
# Ruust sets PORT at run time; the app must read it and bind 0.0.0.0.
CMD ["node", "dist/server.js"]
A small multi-stage Dockerfile. The build stage is discarded, so the runtime image stays lean.

The EXPOSE line is documentation only and does not fix the port; what counts is that your process actually reads PORT and binds 0.0.0.0. Keep the bind in your application code, as below.

javascript
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');
Read PORT from the environment and bind to 0.0.0.0, not localhost.

Ignore what you do not ship

Add a .dockerignore next to the Dockerfile so the build context stays small and secrets or local junk never reach the image. Faster uploads, smaller layers, fewer surprises.

text
node_modules
.git
dist
.env
.env.*
npm-debug.log
A .dockerignore at the repo root keeps the build context tidy.

Environment variables

Start command

With a Dockerfile, the image CMD (or ENTRYPOINT) is authoritative and any Procfile is ignored. A Procfile web: line only affects the default Nixpacks build; on the Dockerfile path it has no effect. To change the start command here, set the CMD/ENTRYPOINT in the image itself.