Skip to main content
Some applications require complex build steps that are difficult to express in a Dockerfile alone. Build scripts (pre.sh and post.sh) let you run custom logic before and after the Docker build.

When to Use Build Scripts

Build scripts are useful when you need to:
  • Clone and configure source code before building
  • Modify configuration files programmatically
  • Run complex build tools not easily containerized
  • Perform post-build validation or artifact processing
  • Set up build environments with specific tool versions
Build scripts run on the host machine (the build server), not inside the Docker container. This gives you full control over the build environment.

Example: ray.so

We’ll use ray.so as our example - a Next.js application for creating code snippets. This app requires cloning, configuration changes, and building with Bun before creating the Docker image.

Project Structure

apps/rayso/
├── meta.json
├── pre.sh          # Runs before Docker build
└── Dockerfile      # Runs after pre.sh

Configuration Files

{
  "name": "rayso",
  "type": "app",
  "title": "ray.so",
  "description": "Create code snippets, browse AI prompts, create extension icons and more.",
  "license": "MIT",
  "variants": {
    "latest": {
      "version": "73fac26",
      "sha": "73fac26b0f184b57ebacd37da95721210a49e1dc",
      "checkver": {
        "type": "sha",
        "repo": "raycast/ray-so"
      }
    }
  }
}

Understanding the Build Process

The build happens in three stages:

Stage 1: Pre-Build Script (pre.sh)

Runs on the host machine before Docker build starts.
#!/bin/bash
set -euxo pipefail

VERSION="73fac26"
  • set -e: Exit immediately if any command fails
  • set -u: Exit if undefined variables are used
  • set -x: Print each command before execution (useful for debugging)
  • set -o pipefail: Ensure pipeline failures are caught
The VERSION variable should match the value in meta.json for consistency.

Stage 2: Docker Build (Dockerfile)

Runs after pre.sh completes successfully.
FROM node:24-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    PORT=3000 \
    HOSTNAME="0.0.0.0"

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs && \
    mkdir -p .next && \
    chown -R nextjs:nodejs /app

USER nextjs

COPY --chown=nextjs:nodejs ./app/public ./public
COPY --chown=nextjs:nodejs ./app/.next/standalone/apps/rayso/app ./
COPY --chown=nextjs:nodejs ./app/.next/static ./.next/static

EXPOSE 3000

CMD ["node", "server.js"]
Key points:
  1. Single stage: No build stage needed since pre.sh handled building
  2. Security: Creates a non-root user (nextjs) to run the application
  3. Minimal copying: Only copies the built artifacts from ./app/ (created by pre.sh)
  4. Standalone structure: Copies from .next/standalone/apps/rayso/app (Next.js monorepo structure)
The Dockerfile has access to the ./app/ directory created by pre.sh. This is why we can copy built files directly.

Build Script Best Practices

Error Handling

Always start your scripts with:
#!/bin/bash
set -euxo pipefail
This ensures:
  • Scripts fail fast on errors
  • Undefined variables are caught
  • Commands are logged for debugging
  • Pipeline failures are detected

Working Directory

Build scripts run in the application directory (e.g., apps/rayso/). Paths are relative to this directory:
# Creates apps/rayso/app/
mkdir -p app

# References files in apps/rayso/
cp config.template app/config.json

Environment Variables

Scripts have access to environment variables from the build system:
  • VERSION: The version from meta.json
  • SHA: The git SHA from meta.json
  • VARIANT: The variant being built
You can reference these in your scripts:
git checkout $SHA
echo "Building version $VERSION"

Clean Builds

Remove artifacts from previous builds:
# Clean before starting
rm -rf app/
mkdir -p app

Dependency Management

Use version managers for reproducible builds:
# Using mise (recommended)
mise use [email protected] -g

# Using nvm
nvm install 20.11.0
nvm use 20.11.0

# Using asdf
asdf install nodejs 20.11.0
asdf global nodejs 20.11.0

Common Build Script Patterns

#!/bin/bash
set -euxo pipefail

# Clone repository
git clone https://github.com/user/repo app
cd app
git checkout $SHA

# Install and build
npm install
npm run build
Best for: Standard Node.js applications

Post-Build Scripts (post.sh)

While this example doesn’t use a post.sh script, here are common use cases:

Validation

#!/bin/bash
set -euxo pipefail

# Test the built image
docker run --rm my-image:latest node --version
docker run --rm my-image:latest npm test

Artifact Publishing

#!/bin/bash
set -euxo pipefail

# Export and publish build artifacts
docker save my-image:latest | gzip > image.tar.gz
aws s3 cp image.tar.gz s3://bucket/images/

Cleanup

#!/bin/bash
set -euxo pipefail

# Clean up large build artifacts
rm -rf app/node_modules
rm -rf app/.next/cache

Debugging Build Scripts

Enable Verbose Output

The -x flag in set -euxo pipefail prints each command:
+ VERSION=73fac26
+ mkdir -p app
+ cd app
+ git clone --depth=1 https://github.com/raycast/ray-so .
Cloning into '.'...

Add Debug Statements

echo "=== Starting build ==="
echo "Version: $VERSION"
echo "Working directory: $(pwd)"

# ... build commands ...

echo "=== Build complete ==="
ls -la app/dist/

Test Locally

Run scripts locally before committing:
cd apps/rayso
bash -x pre.sh

Comparison: Build Scripts vs Dockerfile

AspectBuild Scripts (pre.sh)Dockerfile
Runs onHost machineDocker container
Build toolsAny tools on hostOnly containerized tools
FlexibilityVery highModerate
ReproducibilityDepends on hostHigh (isolated)
SpeedCan be fasterMay need image pulls
Best forComplex builds, config changesStandard containerized builds
Combine both approaches: Use pre.sh for tasks that need host tools, and Dockerfile for containerized final assembly.

Key Takeaways

  • pre.sh runs first: Use it to prepare source code and build artifacts
  • Dockerfile accesses build results: Can copy from directories created by pre.sh
  • Error handling is critical: Always use set -euxo pipefail
  • Keep builds reproducible: Pin tool versions and use git SHAs
  • Debug with -x: The verbose flag helps troubleshoot issues

Next Steps

Simple App Example

See a basic example without build scripts

Multi-Variant Setup

Learn about creating multiple variants

Build docs developers (and LLMs) love