What is a Build Script?

Devops & Infrastructure, Tips & Tricks, Tutorials, and What Is

What is a Build Script?

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:

  1. Install dependencies — pull in the exact versions your app needs
  2. Compile or transpile — turn TypeScript, Sass, or JSX into browser-ready code
  3. Run checks — linting, type checking, tests
  4. Optimise — minify, tree-shake, compress images
  5. 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:

  • -e exits on any error
  • -u treats unset variables as errors (catches typos)
  • -o pipefail catches errors in piped commands (without it, failing-cmd | grep reports 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 --version and match your local version
  • Missing environment variables — add set -u to 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.sh before 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.