New to Rust? Grab our free Rust for Beginners eBook Get it free →
Jenkins CI/CD Pipeline for Node.js: Build, Test and Deploy

Jenkins runs the commands you would type by hand, on a machine that stays up after you close the laptop. I ran a Node.js pipeline on Jenkins 2.568.3 and the build finished green while the deploy step failed twice, once on an SSH public key and once on a PATH that exists only in a login shell.
What a Jenkins pipeline actually automates
A pipeline is a file that names stages, and each stage runs shell commands on a machine Jenkins chooses. The agent declaration picks that machine, the stages group the work, and the steps blocks hold the commands themselves.
The build machine and the deploy target are different machines. Jenkins runs the tests on the first one, and it has no way to reach the second until you hand it a credential.
| Stage | Command it runs | What a green result means |
|---|---|---|
| Install | npm ci | The lockfile resolved on the build machine |
| Test | npm test | The suite exited with status 0 |
| Deploy | Your deploy script over SSH | The deploy command exited with status 0, which is not the same as the app answering |
Prerequisites that decide the setup
A current Jenkins release needs Java 21 or later, and the project’s own installation page puts a small team at 4 GB of memory and 50 GB of disk. The Debian and Ubuntu install now goes through a keyring file rather than the apt-key flow that older write-ups use.
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list
sudo apt update
sudo apt install jenkins
The package starts the service on port 8080 and leaves the first-time unlock password at /var/lib/jenkins/secrets/initialAdminPassword. Everything after that lives in the dashboard, so keep the host reachable from your browser or from a tunnel.
The rest of the list is short, and each item is something the build needs before its first green run.
- Java 21 or later on the Jenkins host, checked with java -version.
- Node.js and npm on the machine that runs the build stage, not only on your laptop, with the same steps as the Node.js and npm setup on Ubuntu.
- An SSH key pair for the deploy target, with the public half in the target’s authorized_keys.
- A repository you can add a webhook to.
A deploy target that does not exist yet is its own piece of work, and hosting a Node.js app on a DigitalOcean server covers the server side of it.
Keep the pipeline in the repository
A Jenkinsfile is the pipeline stored as a file beside the code, so the build definition moves with the repository and gets reviewed like the rest of it. The file below installs a Node.js project from its lockfile and runs the test suite on every build.
pipeline {
agent any
stages {
stage('Install') {
steps { sh 'npm ci' }
}
stage('Test') {
steps { sh 'npm test' }
}
}
}
Install runs npm ci, so the lockfile decides every dependency version. Test runs the suite beside it, and a non-zero exit from that last step fails the build while no later stage runs.
I created that job through the Jenkins REST API and triggered build 1, which finished SUCCESS after npm ci added 116 packages and the suite reported one passing test, and a push produces that same result once the webhook is connected.

The agent any line runs the work on the built-in node. That is fine for one project and wrong once two builds compete for the same disk, so a team setup labels a separate agent and points the Jenkinsfile at that label instead.
Trigger a build from a push
GitHub’s documented path for a CI server is a repository webhook, and the older repository Services screen that some tutorials still describe no longer exists. The Jenkins GitHub plugin listens on one path, so the payload URL is your Jenkins host followed by /github-webhook/.
| Webhook field | Value |
|---|---|
| Payload URL | https://your-jenkins-host/github-webhook/ |
| Content type | application/json |
| Events | Just the push event |
| Jenkins side | Tick GitHub hook trigger for GITScm polling, or add triggers { githubPush() } to the Jenkinsfile |
When Jenkins sits behind a firewall and GitHub cannot reach it, polling is the fallback. Adding pollSCM(‘H/5 * * * *’) inside the same triggers block makes Jenkins ask the repository for changes every few minutes, and the REST endpoint POST /job/<name>/build queues a build and answers 201 when your own scripts want to start one.
Install production dependencies the current way
I ran both forms on the same machine to see the difference in writing. The flag older guides use, npm install –production, now answers with a deprecation warning instead of silence.

The replacement is npm ci, which deletes node_modules first and installs exactly what the lockfile pins, so the build machine cannot drift away from your laptop. Adding –omit=dev drops the packages that only the test run needs, which is what a server install wants.
npm ci # build machine: includes mocha and supertest
npm ci --omit=dev # deploy target: runtime dependencies only
Each machine installs the set it will use, so the test runner stays off the server and the runtime stays out of the test image, and that form installed express 5.2.1 with 65 entries left in node_modules.
Deploy from Jenkins
Jenkins never logs in to your server by itself. It runs the command you give it, and a command that reaches another machine over SSH needs a key that machine accepts.
The credential page asks for three things, and the third one is where first attempts usually fail.
| Field | What goes in it |
|---|---|
| Username | The account on the deploy target, for example deploy |
| Private key | The key pasted in full, BEGIN and END lines included |
| Passphrase | The one that unlocks the key, which pairs with the sshagent step below |
Generate the pair on the Jenkins side with ssh-keygen -t ed25519, then put the public half into the deploy target’s authorized_keys file. Until that key is there, every deploy build stops at the same place: my first attempt over SSH returned Permission denied (publickey), which is the failure the forum threads describe.
The deploy script is four commands, and the second line is the one that gets left out.
#!/bin/sh
export PATH="$HOME/.local/bin:$PATH"
cd /srv/hello-world
npm ci --omit=dev
pm2 restart hello-world
An SSH command is non-interactive, so it inherits a minimal PATH and skips the shell profile that puts node and npm on your path when you log in by hand. I hit that when a script that worked locally answered that npm and pm2 were not found, and the PATH line above the cd is what fixed it.
Run the script from the pipeline through the sshagent step, which loads a stored Jenkins credential into a temporary agent for the duration of the block.
stage('Deploy') {
steps {
sshagent(credentials: ['deploy-target-key']) {
sh './deploy-via-ssh.sh'
}
}
}
I exercised the same hop with an explicit key file rather than a stored credential, so read the sshagent block as the documented way to hand a credential to that script instead of a measured step. The deploy target has to be a known host to the agent, and the key needs no passphrase unless the credential stores one too.

After the restart, the check that matters is the app answering, not the build colour. The process table showed the restart counter moving, and a request to the port returned the page the tests expect.
A web server in front of the app is a separate layer, and the Nginx reverse proxy for Node.js is what passes requests to the port pm2 watches, while the pm2 process table is where a climbing restart counter shows up.
Where a green build stops short
Each failure below produces a build that looks finished, so each one needs a check of its own.
- A green build only means the last command exited 0. A deploy script that exits cleanly without shipping anything reports success, so add a request to the running app at the end of the deploy stage and fail the build when it does not answer.
- A non-interactive SSH session has a shorter PATH than your login shell, which is why npm and pm2 go missing on a script that works when you run it by hand.
- Host key verification runs on the first connection from the agent, so accept the fingerprint once in a controlled way rather than disabling the check permanently.
- Credentials belong in a Jenkins credential and nowhere else. A key printed into the console output is exposed to anyone who can read the build log.
- In fork mode pm2 restart stops the process and starts it again, so requests fail during the changeover. pm2 reload is the one that keeps old workers serving while the new ones come up in cluster mode.
The next command to run after the first green build
A green build proves that the tests passed on the build machine, and the deploy is a separate claim with a separate check. The cheapest way to make that check part of the pipeline is one health request after the restart, which closes the distance between exit status 0 and a working site.
curl -fsS http://your-app-host/health || exit 1
Once that line fails the build for you, the pipeline stops being a test runner and starts being a deployment you can trust on a Friday.
Frequently Asked Questions (FAQs)
How do I set up a Jenkins pipeline for a Node.js project?
Install Jenkins on a host with Java 21 or later, put node and npm on the machine that runs the build, and commit a Jenkinsfile with an install stage that runs npm ci and a test stage that runs npm test. Add a webhook from the repository to the Jenkins URL followed by /github-webhook/ so a push starts the build.
Is npm install –production deprecated?
Yes. Current npm prints npm WARN config production Use –omit=dev instead, and –omit=dev is the replacement. For a CI install, npm ci –omit=dev installs the runtime dependencies from the lockfile and skips devDependencies such as the test runner.
Why does a Jenkins deploy build fail with Permission denied (publickey)?
The build machine is offering a key the deploy target does not accept. Generate the key pair on the Jenkins side, add the public half to the target’s authorized_keys file, and confirm the target is a known host from the agent. A key with a passphrase also needs the sshagent step and the matching passphrase in the credential.
Do I need the Jenkins NodeJS plugin to build a Node.js app?
No. The plugin installs and switches Node.js versions for you, which helps when several jobs need different releases. A single project is fine with node and npm already on the build machine’s PATH, which is the setup in this guide.
Does a successful Jenkins build mean my app was deployed?
Not on its own. Jenkins reports the exit status of the commands it ran, so a deploy stage that exits 0 without shipping anything still shows a green build. Add a request to the running app after the restart and let a failed request fail the build.




