A real build: your own image and npm in the pipeline
We swap echo for a real bundler: a node image, npm ci, esbuild, and dist in the artifacts. We dig into where a job's environment comes from.
What was wrong with the previous build
Until now, the "build" was the cp command. A real project is built with a tool: a bundler, a
compiler, a static site generator. You have to get that tool from somewhere, and that's where a
job property we haven't touched yet comes in: the image it runs in.
Grab the lesson materials:
git pull --no-rebase upstream main
cp -r lesson-07-build/src lesson-07-build/package.json lesson-07-build/package-lock.json .
cp lesson-07-build/pipeline.yml .gitlab-ci.yml
src/ has index.html and app.js; style.css stays in the root from previous lessons.
image: the config sets the environment
Every job runs in a container, and you choose the image:
build:
stage: build
image: node:22.11.0-alpine
script:
- npm ci
- npm run build
Without image, the runner uses its own default image. On GitLab.com's shared runners that's
ruby:3.1: there's no Node or npm in it, and the job fails with sh: npm: not found or a similar
error. On a self-hosted runner, whoever set it up decides the default image. An explicit image
settles the "works on my machine, not on CI" question: the tool version is recorded in the
repository next to the code.
About the tag. node:22.11.0-alpine pins the exact Node version. The tag node:22-alpine (the one
in pipeline.yml from ci-start) moves: today it points to one 22.x minor version, in a month to
another. The build usually doesn't break because of this, but "it built yesterday, not today"
starts exactly this way. That's why the course labs use an exact tag. The alpine variant is used
for its size: node:22.11.0-alpine weighs around 150 MB, while the full node:22.11.0 is several
times heavier. We'll get to what happens with an image with no tag at all in the lesson on
anti-patterns.
npm ci, not npm install
- npm ci
- npm run build
npm ci installs dependencies strictly from package-lock.json and fails if the lock is out of
sync with package.json. It doesn't touch package.json or the lock, and it removes the
node_modules folder before installing. npm install in the same situation would silently patch
the lock and move on, and CI would end up with different versions than you have locally.
The rule is simple: npm install on your machine, npm ci in the pipeline.
The build itself is described in package.json:
"scripts": {
"build": "mkdir -p dist && esbuild src/app.js --bundle --minify --outfile=dist/app.js && cp src/index.html style.css dist/"
}
The pipeline knows nothing about esbuild. It calls npm run build, and the project decides what
exactly builds it. You can swap the tool without touching CI.
Shipping the result as an artifact
artifacts:
paths:
- dist
The build output sits in dist, and Pages expects a public folder, so the deploy stage just
renames it:
pages:
stage: deploy
script:
- mv dist public
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages has no image: node and no dependency install. Node is only needed for the build; deploy
gets by with the default image and one mv command. Every job pulls in exactly the environment it
needs.
How long this takes
There are two packages in the project: esbuild and its platform-specific binary
(@esbuild/linux-x64 or @esbuild/linux-arm64), so npm ci prints added 2 packages and finishes
in seconds. The rest of the job's time goes into pulling the image and starting the container.
On a real frontend with hundreds of dependencies, installing them becomes the longest part of the pipeline. What to do about that is in the next lesson.
Common mistakes
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
The lock is stale: someone changed package.json and didn't commit the updated lock. Further down
in the same log, npm says which package didn't match. Fix it locally: npm install, then commit the
updated package-lock.json.
sh: esbuild: not found
Dependencies weren't installed, or were installed in a different job. Every job starts from a clean
container and a fresh copy of the repository, node_modules from a neighboring job doesn't carry
over. That's why npm ci runs in the same job as the build.
sh: npm: not found: the job has no image with Node, and it runs in the default image.
npm error code EACCES in a homemade image: the build runs as a user without permission on the
folder. The official node image runs as root by default, so this error doesn't happen there.
What to remember
image in a job sets the environment: the tool version lives in the repository, not in the head of
whoever configured the runner. Write an exact tag.
npm ci is mandatory in CI: it installs exactly what's in the lock file and fails on a mismatch.
What exactly gets built is described by the project in package.json, the pipeline only calls
npm run build.
Every job needs its own image and its own steps. Deploy doesn't need Node.
What you'll need for the tasks
The pipeline in the labs runs a learning runner on your laptop. Like a GitLab Runner with the
Docker executor, it runs every job in a clean container with a fresh copy of the commit, and passes
files between stages only through artifacts. Its default image is alpine:3.20.3. The job
containers reach the internet: npm ci downloads packages from registry.npmjs.org.
image: node:22.11.0-alpineon the build job.npm ciandnpm run buildin the same job.git -C work commit -am "...": the runner, like GitLab, only takes what's committed.bash run-pipeline.shandls -R artifacts/: run the pipeline and see what ended up in the artifact.- Running npm without Node on your laptop:
docker runwith thenode:22.11.0-alpineimage and the project folder mounted via-v.
Next we speed up installing dependencies with a cache, and look at how a cache differs from artifacts.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson cicd-practice-07-real-build
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress