When auto-detection isn't enough
Modern platforms auto-detect your framework and pick a build and start command. For a vanilla Vite or Express app, that's perfect. But the moment you have a monorepo, a custom toolchain, a compile step, or a non-standard entrypoint, you need to override both commands explicitly.
This tutorial covers the two commands that matter — build (runs once at deploy time, produces artifacts) and start (runs your process) — and the gotchas for each language.
Build vs. start: the mental model
- Build command runs in the build environment. It compiles, bundles, transpiles, generates clients, and produces the artifacts your app needs. It does *not* keep running.
- Start command runs in the runtime environment. It launches your long-lived process. For a web app it must bind
0.0.0.0and the platform's$PORT.
The single most common deploy failure is a start command that binds localhost or a hardcoded port. Always use $PORT.
Node.js
Most Node apps need a build (TypeScript compile, bundler) and a start that runs the output:
# Build
npm ci && npm run build
# Start (note $PORT, and that it runs the built output, not the source)
node dist/server.jsA frequent mistake is using npm run dev as the start command — that launches a dev server with hot reload, which is wrong for production. Define a real start script:
{
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
}
}And in the server, respect the port:
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0", () => console.log(`listening on ${port}`));Python
Python apps usually have no build step beyond installing dependencies, and a start command that invokes a WSGI/ASGI server:
# Build / install
pip install -r requirements.txt
# Start — FastAPI/ASGI
uvicorn app.main:app --host 0.0.0.0 --port $PORT
# Start — Django/WSGI
gunicorn project.wsgi:application --bind 0.0.0.0:$PORTIf you have a build-time step (collect static files, run migrations), put it in the build command:
pip install -r requirements.txt && python manage.py collectstatic --noinputKeep migrations out of the start command if you scale to multiple instances — otherwise every replica races to migrate. Run migrations as a one-off release step instead.
Go
Go compiles to a single binary, so the build produces it and the start runs it:
# Build
go build -o app ./cmd/server
# Start
./appWith a multi-stage Dockerfile this is even cleaner — the final image just contains the binary.
Monorepos
The trick in a monorepo is running commands from the right subdirectory. Most platforms let you set a root/working directory per service; combine that with workspace-aware commands:
# Build a single app in a pnpm/turbo monorepo
pnpm install --frozen-lockfile
pnpm --filter @acme/api build
# Start
node apps/api/dist/index.jsIf you can set a root directory to apps/api, your commands simplify to the per-package scripts.
Dockerfile: when you want full control
If your build is complicated, a Dockerfile *is* your build and start definition — the platform builds the image and runs its CMD:
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]With a Dockerfile, you usually don't set separate build/start commands in the UI — the image's build stages and CMD define everything. On PandaStack, if a Dockerfile is present it's used directly (built with rootless BuildKit); otherwise buildpacks auto-detect and you can override the install/build/start commands in the service settings.
Setting it on PandaStack
- 1Open your service in the dashboard.
- 2In Build & Deploy settings, override:
- Install command (e.g., npm ci, pip install -r requirements.txt)
- Build command (e.g., npm run build)
- Start command (e.g., node dist/server.js — must use $PORT)
- 1Optionally set a root directory for monorepos.
- 2Redeploy. Watch the live build logs to confirm each command runs.
Debugging a failed override
- App builds but won't start / health check fails: almost always a
$PORTor0.0.0.0binding issue. command not found: the tool isn't installed in the build image — add it to the install step or use a Dockerfile.- Works locally, fails in build: you're relying on something not committed (e.g., a
.env, a global tool, an untracked file). Check the build logs. - Start runs the wrong file: confirm the build actually produced the artifact your start command points to.
Read the live build logs — they tell you exactly which command failed and why. Don't guess.
References
- Heroku Procfile (the original build/start model): https://devcenter.heroku.com/articles/procfile
- Uvicorn deployment: https://www.uvicorn.org/deployment/
- Gunicorn settings: https://docs.gunicorn.org/en/stable/settings.html
- Docker multi-stage builds: https://docs.docker.com/build/building/multi-stage/
- pnpm filtering in monorepos: https://pnpm.io/filtering
---
Need to override build and start commands without fighting your platform? PandaStack lets you set install/build/start per service or just bring a Dockerfile. Try it free at https://dashboard.pandastack.io