How to Build and Deploy a Docker Image with Jenkins

How to Build and Deploy a Docker Image with Jenkins

This is the pipeline pattern I’ve built more times than any other in my career – checkout code, build a Docker image, push it to a registry, deploy it somewhere. It sounds simple, and the basic version is, but getting it production-grade (proper tagging, multi-stage builds, vulnerability scanning, safe rollouts) takes a bit more care. Here’s the version I actually trust in production, not just the minimal demo you see in most tutorials.

The Core Workflow

At its simplest, this pipeline does four things: checkout, build, push, deploy. Each step has real decisions behind it that affect reliability, security, and speed:

  1. Checkout – pull the source code at a specific commit.
  2. Build – run docker build, ideally using a multi-stage Dockerfile to keep the final image small.
  3. Push – tag the image meaningfully (not just latest) and push it to a container registry.
  4. Deploy – update whatever is running the container, whether that’s a single Docker host, a Kubernetes cluster, or an ECS service.

Step 1: Prerequisites

java -jar jenkins-cli.jar -s http://localhost:8080/ install-plugin docker-workflow -restart

Step 2: Write a Proper Multi-Stage Dockerfile

Before the Jenkinsfile even matters, the Dockerfile itself needs to be efficient. Here’s an example for a Node.js app:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

The multi-stage approach keeps build tools and dev dependencies out of the final image, which shrinks image size significantly and reduces the attack surface.

Step 3: Add Registry Credentials to Jenkins

Go to Manage JenkinsCredentialsAdd Credentials, choose Username with password (or the registry-specific credential type), and give it an ID like dockerhub-creds or ecr-creds.

Step 4: Write the Jenkinsfile

pipeline {
    agent any

    environment {
        REGISTRY = 'myregistry.com'
        IMAGE_NAME = "${REGISTRY}/myorg/myapp"
        IMAGE_TAG = "${env.GIT_COMMIT.take(7)}"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build Image') {
            steps {
                script {
                    dockerImage = docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
                }
            }
        }

        stage('Scan Image') {
            steps {
                sh "trivy image --severity HIGH,CRITICAL --exit-code 1 ${IMAGE_NAME}:${IMAGE_TAG}"
            }
        }

        stage('Push Image') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY}", 'dockerhub-creds') {
                        dockerImage.push("${IMAGE_TAG}")
                        dockerImage.push('latest')
                    }
                }
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps {
                sh """
                    kubectl set image deployment/myapp myapp=${IMAGE_NAME}:${IMAGE_TAG} -n production
                    kubectl rollout status deployment/myapp -n production --timeout=120s
                """
            }
        }
    }

    post {
        failure {
            echo 'Pipeline failed - image was not deployed.'
        }
        cleanup {
            sh "docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true"
        }
    }
}

Tagging Strategy: Why Not Just “latest”

Tagging every image latest makes rollbacks nearly impossible – you lose the ability to know exactly what’s running. A better strategy tags images with something traceable:

I usually push both the SHA-tagged image (for traceability) and latest (for convenience in dev/staging), but never deploy production off latest directly.

Step 5: Building for Multiple Architectures (ARM + x86)

If you need images that run on both Apple Silicon dev machines and x86 production servers, use Buildx:

stage('Build Multi-Arch Image') {
    steps {
        sh '''
            docker buildx create --use --name multiarch-builder || true
            docker buildx build \
              --platform linux/amd64,linux/arm64 \
              -t ${IMAGE_NAME}:${IMAGE_TAG} \
              --push .
        '''
    }
}

Note that Buildx pushes directly as part of the build command when doing multi-arch builds, since manifest lists can’t be built and pushed as separate steps the way single-arch images can.

Step 6: Deploying to Different Targets

Deploying to a single Docker host via SSH:

stage('Deploy to Docker Host') {
    steps {
        sshagent(['prod-ssh-key']) {
            sh """
                ssh deploy@prod-server "docker pull ${IMAGE_NAME}:${IMAGE_TAG} && \
                docker stop myapp || true && docker rm myapp || true && \
                docker run -d --name myapp -p 3000:3000 --restart unless-stopped ${IMAGE_NAME}:${IMAGE_TAG}"
            """
        }
    }
}

Deploying to Kubernetes (as shown in the main Jenkinsfile above using kubectl set image), or more robustly with Helm:

stage('Deploy with Helm') {
    steps {
        sh "helm upgrade myapp ./charts/myapp --set image.tag=${IMAGE_TAG} --namespace production --wait --timeout 3m"
    }
}

Deploying to AWS ECS:

stage('Deploy to ECS') {
    steps {
        sh """
            aws ecs register-task-definition --cli-input-json file://taskdef.json
            aws ecs update-service --cluster prod-cluster --service myapp-service --force-new-deployment
        """
    }
}

Integrating with the Wider Toolchain

Monitoring and Troubleshooting

Security Best Practices

Best Practices

FAQs

Should the build and deploy happen in the same pipeline, or be separated? For simpler setups, one pipeline is fine; for stricter environments, many teams split “build and push” from “deploy,” using a separate approval-gated pipeline for production deployment.

How do I handle rollbacks? Because every image is tagged with a traceable identifier, rolling back is just redeploying the previous known-good tag – kubectl set image or helm rollback to the prior release.

Can I build the image without Docker installed on the Jenkins agent? Yes, using Kaniko or Buildah, which build OCI-compliant images without needing a Docker daemon – useful in Kubernetes environments where privileged containers are restricted.

How do I avoid pushing broken images to a shared registry? Run tests and vulnerability scans as gating stages before the push stage, so nothing merges into the shared registry namespace without passing quality checks first.

What’s the best way to manage environment-specific configuration in the image? Bake in environment-agnostic images and inject configuration at runtime via environment variables or mounted config, rather than building separate images per environment.

Summary

A solid Docker build-and-deploy pipeline in Jenkins comes down to a lean multi-stage Dockerfile, traceable image tagging, a mandatory vulnerability scan before pushing, and a deploy stage matched to your actual infrastructure – whether that’s a single host, Kubernetes, or ECS. Once this pattern is dialed in, shipping a new version of your app becomes a matter of merging code, not remembering a checklist of manual deployment steps.

References

Exit mobile version