Build scripts are the glue between your code and a running application. Every time you deploy, something has to install dependencies, compile assets, run tests, and package the result. A build script is that something — a repeatable set of commands that turns source code into a deployable artifact.
If you've ever typed npm install && npm run build before deploying, you've already written a build script. The question is whether you've formalised it.
What a Build Script Actually Does
A build script automates the preparation steps your application needs before it can run in production. The specifics vary by stack, but the pattern is always the same:
- Install dependencies — pull in the exact versions your app needs
- Compile or transpile — turn TypeScript, Sass, or JSX into browser-ready code
- Run checks — linting, type checking, tests
- Optimise — minify, tree-shake, compress images
- Package — produce the final output directory or container image
Without a build script, these steps happen manually — or worse, differently each time. A developer might forget to run tests. A staging build might use different Node flags than production. Build scripts eliminate that variance.
Why Build Scripts Matter for Deployment
The real value isn't automation for its own sake. It's reproducibility. When your deployment pipeline runs the same script every time, you know exactly what's happening. When something breaks, you can reproduce the failure locally by running the same commands.
This is especially critical when multiple people deploy. Without a shared build script, each developer's mental model of how to deploy
drifts over time. The build script is the single source of truth.
Common Build Script Operations
Dependency Installation
Every build starts with dependencies. The key is using lockfile-based installs (npm ci instead of npm install, composer install instead of composer update) to ensure deterministic builds:
# Node.js — use ci for deterministic installs
npm ci
# PHP
composer install --no-dev --optimize-autoloader
# Python
pip install -r requirements.txt
# Ruby
bundle install --deployment --without development test
The --no-dev and --without development flags matter — production builds shouldn't include development dependencies. They add weight, increase attack surface, and occasionally cause conflicts.
Asset Compilation
Modern frontends need a build step. The tooling has evolved significantly — Vite has largely replaced Webpack for new projects, and Bun can handle both package management and bundling:
# Vite (most common for new projects)
npx vite build
# Next.js
npx next build
# Laravel Mix / Vite
npx vite build
# Bun (handles both install and build)
bun install --frozen-lockfile
bun run build
Testing and Quality Checks
Build scripts should fail fast. Run the cheapest checks first — linting catches syntax issues in seconds, while a full test suite might take minutes:
# Fast checks first
npm run lint
npx tsc --noEmit # Type checking (no output)
# Then tests
npm run test -- --ci # CI mode: no watch, fail on missing snapshots
The --ci flag matters. Without it, test runners often default to interactive watch mode, which hangs your build indefinitely.
Writing a Build Script That Works
Here's a production-ready build script for a Node.js application. Notice the error handling — every build script should fail immediately on any error rather than silently continuing with a broken state:
#!/bin/bash
set -euo pipefail
echo "=== Installing dependencies ==="
npm ci
echo "=== Running linter ==="
npm run lint
echo "=== Type checking ==="
npx tsc --noEmit
echo "=== Running tests ==="
npm run test -- --ci
echo "=== Building for production ==="
NODE_ENV=production npm run build
echo "=== Build complete ==="
The set -euo pipefail line is critical:
-eexits on any error-utreats unset variables as errors (catches typos)-o pipefailcatches errors in piped commands (without it,failing-cmd | grepreports success)
Conditional Logic for Different Environments
A single build script can handle multiple environments. Use environment variables — not separate scripts — to control behaviour:
#!/bin/bash
set -euo pipefail
npm ci
if [ "${NODE_ENV:-}" = "production" ]; then
npm run build:prod
npm prune --production # Remove devDependencies after build
else
npm run build:dev
fi
Using Build Scripts in DeployHQ
DeployHQ runs your build script automatically as part of every deployment. In your project's Build Settings, you specify the commands — DeployHQ handles the execution environment, including Node.js, PHP, Ruby, and Python runtimes.
A typical DeployHQ build configuration looks like this:
npm ci
npm run build
DeployHQ runs these commands in a clean environment before transferring files to your server. This means your server never needs Node.js, npm, or build tools installed — it only receives the compiled output.
For more complex setups, you can use deployment scripts that run on the server after file transfer — handling tasks like database migrations, cache clearing, or service restarts.
Build Variables
DeployHQ provides environment variables you can reference in build scripts:
# Available in DeployHQ builds
echo "Deploying branch: $DEPLOY_REF"
echo "Environment: $DEPLOY_SERVER_NAME"
# Use server name for conditional logic
if [ "$DEPLOY_SERVER_NAME" = "Production" ]; then
npm run build:prod
else
npm run build:staging
fi
Common Mistakes
Running npm install instead of npm ci. npm install can modify your lockfile, producing non-deterministic builds. npm ci installs exactly what's in package-lock.json and is faster in CI environments.
Not failing fast. If your build script runs tests after a 10-minute compilation step, you waste time on every failing test. Run cheap checks (linting, type checking) before expensive ones (compilation, tests).
Cleaning up too aggressively. Removing node_modules at the end of a build script might seem tidy, but it breaks deployments that need those modules at runtime (e.g., Node.js servers). Only prune if you're deploying compiled static assets.
Hardcoding paths and versions. Use environment variables for anything that changes between environments. Don't embed /home/deploy/app in your build script — use $DEPLOY_ROOT or $(pwd).
Ignoring exit codes. Some commands fail silently. Always use set -e and check critical operations explicitly:
if ! npm run test -- --ci; then
echo "Tests failed — aborting build"
exit 1
fi
Debugging Build Failures
When a build fails in your deployment pipeline but works locally, the problem is almost always environmental. Here's a systematic approach:
# Print environment for debugging
node --version
npm --version
echo "NODE_ENV=$NODE_ENV"
# Time each step to find bottlenecks
time npm ci
time npm run build
Common culprits:
- Different Node.js versions — check with
node --versionand match your local version - Missing environment variables — add
set -uto catch unset variables immediately - Memory limits — CI environments often have less RAM. Increase with
export NODE_OPTIONS="--max-old-space-size=4096" - Permission errors — run
chmod +x build.shbefore execution
For a comprehensive pre-deployment checklist, see our ultimate deployment checklist.
Build Scripts vs Build Pipelines
A build script is a single file with sequential commands. A build pipeline is a broader concept — it orchestrates multiple stages (build, test, deploy) with features like parallel execution, caching, and conditional steps.
Build scripts are the foundation. Pipelines build on top of them. Even in a sophisticated CI/CD pipeline, each stage ultimately runs a build script.
DeployHQ runs your build scripts automatically and deploys the result to your servers — SFTP, SSH, S3, or cloud platforms. Start deploying for free.
Have questions? Reach out at support@deployhq.com or find us on X/Twitter.