--- title: "How to Build and Push Docker Images with a Jenkins Pipeline" description: "Write a declarative Jenkinsfile that builds a Docker image, tests it inside the container and pushes it to Docker Hub or a private registry on each commit." category: "CI/CD DevOps" language: en source: https://cubepath.com/docs/ci-cd-devops/jenkins-pipeline-for-docker-builds updated: 2026-09-25 --- # How to Build and Push Docker Images with a Jenkins Pipeline Jenkins pipelines are defined in a `Jenkinsfile` that lives in the same repository as your code, so the build process is versioned and reviewed like everything else. In this tutorial you will give Jenkins access to Docker on an Ubuntu 24.04 server, store registry credentials safely, and write a declarative pipeline that builds an image, runs the tests inside it, checks that the container starts, and pushes it to Docker Hub or a private registry. A small Python application is used as the example, but the pipeline works the same for any project with a `Dockerfile`. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS with at least 2 GB of RAM (4 GB recommended), for example a CubePath VPS, and a non-root user with `sudo` privileges. - Jenkins LTS installed from the official Jenkins repository, following the Linux instructions at `jenkins.io/doc/book/installing/linux/`, with the suggested plugins (they include Pipeline, Git and Credentials Binding) and an admin account. - Docker Engine installed on the same server from Docker's official repository. - A Git repository (GitHub, GitLab or similar) that Jenkins can clone. - A Docker Hub account and a personal access token with read and write access, or credentials for a private registry. ## Step 1 - Giving Jenkins access to Docker The pipeline runs `docker` commands as the `jenkins` system user, which needs permission to use the Docker socket. Add it to the `docker` group and restart Jenkins so the new group membership takes effect: ```bash sudo usermod -aG docker jenkins sudo systemctl restart jenkins ``` Verify that the `jenkins` user can talk to the Docker daemon: ```bash sudo -u jenkins docker version --format '{{.Server.Version}}' ``` ```output 28.4.0 ``` If you get `permission denied while trying to connect to the Docker daemon socket`, Jenkins was not restarted or the group was not added. > Warning: Membership in the `docker` group is equivalent to root access on the host, because any job can start a privileged container. This is acceptable when only trusted people can change pipelines. For shared Jenkins installations, run Docker builds on a dedicated build agent instead of the controller. ## Step 2 - Preparing the example application If you are using your own project, skip to Step 3. Otherwise, create these three files at the root of your repository. `app.py` is a minimal web server with a health endpoint: ```python from http.server import BaseHTTPRequestHandler, HTTPServer def message(path): if path == "/health": return "ok" return "Hello from Jenkins" class Handler(BaseHTTPRequestHandler): def do_GET(self): body = (message(self.path) + "\n").encode() self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) if __name__ == "__main__": HTTPServer(("0.0.0.0", 8000), Handler).serve_forever() ``` `test_app.py` contains its unit tests: ```python import unittest from app import message class MessageTest(unittest.TestCase): def test_health(self): self.assertEqual(message("/health"), "ok") def test_root(self): self.assertIn("Hello", message("/")) if __name__ == "__main__": unittest.main() ``` The `Dockerfile` builds the image and runs the application as a non-root user: ```dockerfile FROM python:3.12-slim WORKDIR /app COPY app.py test_app.py ./ RUN useradd --system --no-create-home app USER app EXPOSE 8000 CMD ["python", "app.py"] ``` Before involving Jenkins, check that the image builds and the tests pass on your machine: ```bash docker build -t hello-jenkins:dev . docker run --rm hello-jenkins:dev python -m unittest -v ``` ```output test_health (test_app.MessageTest.test_health) ... ok test_root (test_app.MessageTest.test_root) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK ``` ## Step 3 - Storing the registry credentials in Jenkins The pipeline must log in to the registry without the token appearing in the Jenkinsfile or in the build log. Jenkins' credentials store handles both. In the Jenkins web interface: 1. Go to **Manage Jenkins > Credentials**. 2. Under **Stores scoped to Jenkins**, click **System**, then **Global credentials (unrestricted)**. 3. Click **Add Credentials** and choose the kind **Username with password**. 4. Enter your Docker Hub username as **Username**, the personal access token as **Password**, and `dockerhub` as **ID**. 5. Click **Create**. The ID is how the pipeline refers to these credentials. If the repository is private, add a second credential for Git the same way (a token or an SSH key) to select when you create the job. ## Step 4 - Writing the Jenkinsfile Create a file called `Jenkinsfile` at the root of the repository. Replace `your_dockerhub_user` with your Docker Hub username: ```groovy pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '20')) disableConcurrentBuilds() timeout(time: 30, unit: 'MINUTES') } triggers { pollSCM('H/5 * * * *') } environment { IMAGE = 'your_dockerhub_user/hello-jenkins' TAG = "${env.BUILD_NUMBER}" } stages { stage('Build') { steps { sh 'docker build --pull --label org.opencontainers.image.revision="$GIT_COMMIT" -t "$IMAGE:$TAG" .' } } stage('Unit tests') { steps { sh 'docker run --rm "$IMAGE:$TAG" python -m unittest -v' } } stage('Smoke test') { steps { sh ''' cid=$(docker run -d "$IMAGE:$TAG") trap 'docker rm -f "$cid" > /dev/null' EXIT for i in $(seq 1 10); do if docker exec "$cid" python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"; then exit 0 fi sleep 1 done docker logs "$cid" exit 1 ''' } } stage('Push') { steps { withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_TOKEN')]) { sh ''' echo "$REGISTRY_TOKEN" | docker login -u "$REGISTRY_USER" --password-stdin docker tag "$IMAGE:$TAG" "$IMAGE:latest" docker push "$IMAGE:$TAG" docker push "$IMAGE:latest" ''' } } } } post { always { sh 'docker logout || true' sh 'docker image rm "$IMAGE:$TAG" "$IMAGE:latest" || true' } } } ``` What each part does: - **`agent any`:** runs on any available executor, here the Jenkins server itself, where Docker is installed. - **`options`:** keeps the last 20 builds, prevents two builds of this job from running at the same time (they would share the Docker login) and aborts builds that hang. - **`triggers`:** checks the repository for new commits about every 5 minutes. `H` spreads the checks so all jobs do not poll at the same minute. - **`environment`:** the image name and a tag based on the Jenkins build number, so every build produces a unique, traceable image. The Git commit is stored as an OCI label. - **Build:** `--pull` always fetches the latest base image, so security fixes in `python:3.12-slim` reach your builds. - **Unit tests:** runs the tests inside the image you just built, with exactly the dependencies that will go to production. - **Smoke test:** starts the container and waits until `/health` answers, which catches images that build but fail on startup. The `trap` removes the container whether the test passes or fails. - **Push:** `withCredentials` exposes the token only inside its block and masks it in the log. `--password-stdin` keeps it out of the process list. The `sh` scripts use single quotes so the shell, not Groovy, expands the variables, which is what Jenkins recommends for secrets. - **`post { always }`:** logs out and removes the local images so the server's disk does not fill up with one image per build. Commit and push all the files: ```bash git add Jenkinsfile Dockerfile app.py test_app.py git commit -m "Add Docker build pipeline" git push origin main ``` ## Step 5 - Creating the pipeline job In Jenkins, create a job that reads its pipeline from the repository: 1. Click **New Item**, enter `hello-jenkins` as the name, select **Pipeline** and click **OK**. 2. In the **Pipeline** section, set **Definition** to **Pipeline script from SCM**. 3. Set **SCM** to **Git** and enter the repository URL. Select the Git credentials if the repository is private. 4. Set **Branch Specifier** to `*/main` and leave **Script Path** as `Jenkinsfile`. 5. Click **Save**. Jenkins registers the `pollSCM` trigger from the Jenkinsfile the first time the job runs, so start the first build by hand with **Build Now**. ## Step 6 - Running the pipeline and verifying the image Open the build from **Build History** and click **Console Output**. A successful run ends like this: ```output + docker push your_dockerhub_user/hello-jenkins:1 The push refers to repository [docker.io/your_dockerhub_user/hello-jenkins] ... 1: digest: sha256:4f6c1e... size: 1573 ... + docker logout Removing login credentials for https://index.docker.io/v1/ ... Finished: SUCCESS ``` The stage view on the job page shows each stage in green with its duration. Now check the published image from any machine with Docker: ```bash docker run -d --rm --name hello-test -p 8000:8000 your_dockerhub_user/hello-jenkins:1 curl http://localhost:8000/ ``` ```output Hello from Jenkins ``` Stop the test container with `docker stop hello-test`. From now on, every commit pushed to `main` is picked up within five minutes and produces a new tagged image. To build immediately on each push instead of polling, install the **GitHub** plugin, check **GitHub hook trigger for GITScm polling** in the job, and add a webhook in the repository pointing to `https://jenkins.your_domain/github-webhook/`. ## Pushing to a private registry To push to a self-hosted registry such as Harbor or the Docker registry, only two things change. Create a `Username with password` credential with the registry account (for example with the ID `private-registry`), then prefix the image name with the registry host and pass the host to `docker login` and `docker logout`: ```groovy environment { IMAGE = 'registry.your_domain/your_project/hello-jenkins' TAG = "${env.BUILD_NUMBER}" } ``` ```groovy withCredentials([usernamePassword(credentialsId: 'private-registry', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_TOKEN')]) { sh ''' echo "$REGISTRY_TOKEN" | docker login registry.your_domain -u "$REGISTRY_USER" --password-stdin docker tag "$IMAGE:$TAG" "$IMAGE:latest" docker push "$IMAGE:$TAG" docker push "$IMAGE:latest" ''' } ``` In `post`, change the logout to `docker logout registry.your_domain || true`. The registry must have a valid TLS certificate; Docker refuses to push over plain HTTP by default. ## Troubleshooting - **`permission denied while trying to connect to the Docker daemon socket`:** the `jenkins` user is not in the `docker` group, or Jenkins was not restarted after adding it (Step 1). - **`denied: requested access to the resource is denied` on push:** the image name does not start with your Docker Hub username or organization, or the access token is read-only. - **`ERROR: Could not find credentials entry with ID 'dockerhub'`:** the credential ID in Jenkins does not match `credentialsId` in the Jenkinsfile, or it was created in a folder the job cannot see. - **The server runs out of disk space:** build cache and base images still accumulate. Check with `docker system df` and schedule `docker builder prune --force --filter until=168h` in cron or a systemd timer. ## Conclusion You now have a pipeline, stored in the repository, that builds a Docker image on every commit, tests it inside the container, verifies that it starts and pushes a uniquely tagged image to a registry without exposing the token. Next, add an image vulnerability scan stage before the push, move Docker builds to a dedicated Jenkins agent, or add a deploy stage that pulls the new tag on your servers over SSH.