Running esbuild builds inside Docker guarantees that your compiled JavaScript artifacts are identical regardless of the developer’s machine or the CI environment.
Let’s see esbuild in action, building a simple React application within a Docker container.
First, we need a package.json for our project:
{
"name": "my-esbuild-app",
"version": "1.0.0",
"scripts": {
"build": "esbuild src/index.tsx --bundle --outfile=dist/bundle.js --format=esm --platform=browser --define:process.env.NODE_ENV='production'"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"esbuild": "^0.19.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15"
}
}
And a basic src/index.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
function App() {
return <h1>Hello from esbuild in Docker!</h1>;
}
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Now, let’s create a Dockerfile to run this:
FROM node:20-alpine as builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev # Install only production dependencies for a smaller image
COPY . .
RUN npm run build
This Dockerfile uses node:20-alpine as its base. It sets the working directory to /app, copies the package.json and package-lock.json, installs only production dependencies (since esbuild is a dev dependency), copies the rest of the application source, and then runs the npm run build script which executes esbuild.
To build and run this:
- Save the files: Create a directory for your project, save the
package.json,src/index.tsx, andDockerfileinside it. Make surepackage-lock.jsonis also present. - Build the Docker image:
docker build -t esbuild-app-builder . - Run the build and extract the artifact:
This command runs thedocker run --rm -v "$(pwd)/dist:/app/dist" esbuild-app-builderesbuild-app-builderimage. The--rmflag cleans up the container after it exits. The-v "$(pwd)/dist:/app/dist"part is crucial: it mounts your localdistdirectory (creating it if it doesn’t exist) to/app/distinside the container. Whennpm run buildexecutes and writesdist/bundle.js, it will appear in your localdistdirectory.
After running the docker run command, you’ll find bundle.js in your local dist folder.
The core problem esbuild solves here is the wildly varying JavaScript build times and potential dependency mismatches across different developer machines or CI environments. By containerizing the build process, you create a consistent, isolated environment. The node:20-alpine image provides a predictable Node.js runtime, and npm ci ensures exact dependency versions from the package-lock.json. esbuild itself is then executed within this controlled ecosystem.
The FROM node:20-alpine as builder syntax creates a multi-stage build. This means the final Docker image can be much smaller. If you were to build the application directly within the image and then copy the artifacts out, you could discard the builder stage entirely, leaving only the compiled output or a minimal runtime image. For this example, we’re extracting the artifact externally using a volume mount, so the builder image is sufficient.
The --define:process.env.NODE_ENV='production' flag in the package.json script is a common esbuild pattern. It tells esbuild to replace all occurrences of process.env.NODE_ENV with the literal string 'production' during the build. This is important because many libraries (like React) perform optimizations when NODE_ENV is set to production, and this flag ensures those optimizations are applied even if process.env.NODE_ENV isn’t set in the Docker build environment itself.
The most surprising thing about esbuild’s performance, even when run inside Docker, is how little the containerization overhead impacts its speed. esbuild is written in Go and compiled to a native binary, meaning it doesn’t rely on the Node.js runtime for its own execution, only for its interface and dependency management if you’re invoking it via npm. This makes its startup time incredibly fast, often measured in milliseconds, even when dealing with large codebases. When you run npm run build inside Docker, you’re essentially just launching a Go binary, which is highly efficient.
The next step you’ll likely consider is optimizing your Dockerfile further by using multi-stage builds to create a minimal final image containing only your compiled assets, rather than relying on volume mounts to extract them.