Node.js on Previewops
Previewops builds whatever your Dockerfile produces and runs it as a container, so Node.js needs no special support. What it does need is what every stack needs: read the injected PORT and listen on 0.0.0.0.
| Read the port | process.env.PORT |
| Bind all interfaces | app.listen(port, '0.0.0.0') |
Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV PORT=8080
CMD ["node", "dist/index.js"]
ENV PORT=8080 is only a default. Previewops overrides it at deploy time, so the app must read the value rather than assume it.
Application code
A typical setup with Express:
const express = require('express');
const app = express();
// Read the injected PORT. Do not hardcode it — Previewops chooses the port
// and points its proxy at that same value.
const port = Number(process.env.PORT) || 8080;
app.get('/', (_req, res) => res.send('ok'));
// '0.0.0.0' is the important part. Passing 'localhost' or '127.0.0.1' makes
// the container unreachable from the proxy.
app.listen(port, '0.0.0.0', () => console.log('listening on ' + port));
Node.js-specific pitfalls
- Next.js
next startignores a bind address by default. Pass-H 0.0.0.0 -p $PORTexplicitly. CMD ["npm", "start"]swallows signals. PreferCMD ["node", "dist/index.js"]so the container stops cleanly instead of being killed after a timeout.- Dev dependencies in the runtime image. Use a multi-stage build, or
npm ci --omit=devin the final stage, to keep the image small and the build fast.
Configuration
If your Dockerfile is not at the repository root, point at it from .previewops.yaml:
provider: cloud-run
dockerfile: ./docker/Dockerfile
port: 8080
ttl_hours: 24
See configuration.md for every available option.
Do not gate startup behind a migration
Language-agnostic, and the hardest failure here to diagnose:
# Avoid this
CMD ["sh", "-c", "run-migrations && start-server"]
If the migration cannot reach the database — a paused instance, an expired connection string, a cold-start timeout — it never exits, the server never starts, and the port never opens. The deploy itself already succeeded, so the preview shows as Live while nothing answers. Run migrations as a separate, visible step instead.
Related pages
- getting-started.md — your first deploy end to end
- configuration.md —
.previewops.yamlreference - databases.md — connecting a database to a preview
- ui-deploy.md — deploying from the dashboard