Back to Blog
Tutorial9 min read2026-07-07

How to Set a Custom Build and Start Command

Auto-detection covers the common case, but real apps need custom build and start commands. Here's how to override both correctly, with patterns for Node, Python, Go, and Docker.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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.0 and 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.js

A 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:$PORT

If 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 --noinput

Keep 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
./app

With 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.js

If 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

  1. 1Open your service in the dashboard.
  2. 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)

  1. 1Optionally set a root directory for monorepos.
  2. 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 $PORT or 0.0.0.0 binding 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

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also