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:
- Checkout – pull the source code at a specific commit.
- Build – run
docker build, ideally using a multi-stage Dockerfile to keep the final image small. - Push – tag the image meaningfully (not just
latest) and push it to a container registry. - Deploy – update whatever is running the container, whether that’s a single Docker host, a Kubernetes cluster, or an ECS service.
Step 1: Prerequisites
- Jenkins with the Docker Pipeline Plugin installed.
- Docker available to the Jenkins agent, either via socket mounting or a dedicated Docker-capable agent (see the earlier article on running Jenkins in Docker for the tradeoffs).
- Access to a container registry – Docker Hub, AWS ECR, Google Artifact Registry, or a private registry like Harbor.
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 Jenkins → Credentials → Add 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:
- Git commit SHA (
myapp:a1b2c3d) – always unique, always traceable back to exact source. - Semantic version (
myapp:1.4.2) – for release builds, tied to a Git tag. - Branch + build number (
myapp:main-142) – useful for staging environments tracking ongoing work.
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
- Git/GitHub – use
env.GIT_COMMITfor tagging and trigger builds via webhooks on push/PR/tag events. - Kubernetes – the natural deployment target for containerized apps; combine with Helm for templated, versioned releases.
- Terraform – provision the registry, cluster, and networking infrastructure ahead of time, so the Jenkins pipeline only handles application deployment, not infra provisioning.
- Security tools – integrate Trivy, Grype, or Snyk Container as a mandatory scanning gate before any image is pushed to a shared registry.
- Slack – notify the team the moment a new image is deployed to production, including the commit SHA and a rollback command for convenience.
Monitoring and Troubleshooting
- Build succeeds but deploy fails – check that the Jenkins agent’s Kubernetes/AWS credentials have the exact RBAC/IAM permissions needed; “succeeded to build, failed to deploy” is almost always a permissions gap, not a code issue.
- Image pull errors on the target – verify the target environment has valid registry credentials (
imagePullSecretsin Kubernetes, ordocker loginon a plain host). - Vulnerability scan blocking every build – tune your Trivy/Grype severity threshold; blocking on every medium-severity CVE in base images often creates more noise than security value.
- Disk filling up on the Jenkins agent – Docker image layers accumulate fast; run
docker system prune -af --filter "until=24h"on a schedule.
Security Best Practices
- Scan every image before pushing, and fail the pipeline on critical/high vulnerabilities in application code layers (be more lenient with base OS layers you don’t control directly).
- Use short-lived, scoped registry credentials (e.g., IAM roles for ECR) instead of long-lived static tokens where the registry supports it.
- Run containers as a non-root user (
USER nodein the Dockerfile example above) rather than defaulting to root. - Sign images with tools like Cosign/Sigstore if your deployment target enforces image provenance verification.
Best Practices
- Always use multi-stage Dockerfiles to minimize final image size and attack surface.
- Tag images with immutable, traceable identifiers (commit SHA or semver), never rely solely on
latest. - Keep build and deploy as separate, independently retryable stages – a flaky deploy shouldn’t force you to rebuild the image from scratch.
- Use
docker system prunescheduling and registry retention policies to avoid unbounded storage growth over time.
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
- Docker Pipeline Plugin documentation: https://plugins.jenkins.io/docker-workflow/
- Docker Buildx documentation: https://docs.docker.com/build/buildx/
- Trivy vulnerability scanner: https://aquasecurity.github.io/trivy/
- Kubernetes kubectl reference: https://kubernetes.io/docs/reference/kubectl/
- Helm documentation: https://helm.sh/docs/