Live workshop Q&A app built on Zerops — participants ask questions from their phones, facilitators moderate and answer from a shared queue. Includes open registration, role-based access (participant, moderator, admin), QR join flow, PostgreSQL, and Next.js 16 SSR on the Zerops platform.
Workshop demo for Zerops and ZCP. Source: kriszerops/zerops-workshop.
The main application configuration file you place at the root of your repository, it tells Zerops how to build, deploy and run your application.
zerops:
# 'prod' compiles an optimized Next.js standalone bundle for
# deployment. 'dev' deploys full source so you can SSH in and
# run the Next.js dev server interactively.
- setup: prod
build:
base: nodejs@22
buildCommands:
# npm ci installs exact locked versions - reproducible builds.
- npm ci
# Bundle migrate.js + pg into a self-contained migrate.cjs
# so initCommands can run it without NODE_PATH gymnastics.
# pg is traced by @vercel/nft into .next/standalone/node_modules
# but migrate.js runs before the server starts, so bundling is
# the cleanest solution.
- node scripts/bundle-migrate.mjs
# prebuild script (generate-build-info.js) writes version +
# timestamp to build-info.json, then next build compiles the
# standalone bundle with @vercel/nft import tracing.
- npm run build
# Next.js standalone bundles all runtime dependencies via
# @vercel/nft - no node_modules needed at runtime. Static
# assets and public dir must be deployed alongside.
deployFiles:
- .next/standalone
- .next/static
- public
- build-info.json
- migrate.cjs
cache:
# Do NOT cache .next/cache - Zerops cache restoration causes
# EACCES permission errors on subsequent builds.
- node_modules
# Readiness check verifies the new container responds before
# the project balancer routes traffic to it.
deploy:
readinessCheck:
httpGet:
port: 3000
path: /
run:
base: nodejs@22
initCommands:
# zsc execOnce ensures migration runs exactly once per
# deploy version across all containers - prevents race
# conditions when minContainers > 1.
# migrate.cjs is esbuild-bundled with pg included, so it
# runs without any external node_modules.
- zsc execOnce ${appVersionId} -- node migrate.cjs
ports:
- port: 3000
httpSupport: true
envVariables:
NODE_ENV: production
DB_NAME: db
# Referencing variables: ${hostname_key} resolves to
# the value generated by Zerops for the 'db' service.
DB_HOST: ${db_hostname}
DB_PORT: ${db_port}
DB_USER: ${db_user}
DB_PASS: ${db_password}
# Standalone server.js reads PORT env var automatically.
start: node .next/standalone/server.js
- setup: dev
build:
base: nodejs@22
# Ubuntu gives a richer toolset for interactive SSH work
# (git, curl, editors) compared to Alpine default.
os: ubuntu
buildCommands:
# npm install (not npm ci) - dev may lack a lock file or
# need flexible resolution during active development.
- npm install
# Deploy the full working directory so the developer has
# source code, node_modules, and config ready on SSH entry.
deployFiles: ./
cache:
- node_modules
run:
base: nodejs@22
os: ubuntu
initCommands:
# Migration runs on first deploy so the database is ready
# when the developer SSHs in. Dev has node_modules deployed
# alongside source, so migrate.js uses pg from node_modules.
- zsc execOnce ${appVersionId} -- node migrate.js
ports:
- port: 3000
httpSupport: true
envVariables:
NODE_ENV: development
DB_NAME: db
DB_HOST: ${db_hostname}
DB_PORT: ${db_port}
DB_USER: ${db_user}
DB_PASS: ${db_password}
# zsc noop keeps the container alive. The developer starts
# `npm run dev` manually via SSH.
start: zsc noop --silentStandalone mode (output: 'standalone' in next.config.ts): Next.js traces all imports at build time using @vercel/nft and bundles only the required modules into .next/standalone/. No node_modules directory is needed at runtime — the standalone folder is self-contained.
Three deploy artifacts: Standalone mode does NOT automatically include .next/static/ or public/ inside the standalone folder. All three must be listed separately in deployFiles so the runtime server can find them.
Migration bundling: The migration script (migrate.js) needs the pg driver, which lives inside .next/standalone/node_modules/ at runtime. Rather than using NODE_PATH env var workarounds, we use esbuild in buildCommands to bundle migrate.js + pg into a self-contained migrate.cjs that runs anywhere.
zsc execOnce: Production deployments run multiple containers. Without zsc execOnce ${appVersionId}, every container would run the migration simultaneously — causing race conditions. execOnce ensures exactly one container runs the migration per deploy version; all others wait.
No .next/cache in build cache: Zerops cache restoration sets file ownership that triggers EACCES permission errors when Next.js tries to write to the cache directory on the next build. Only cache node_modules.