Bob's Journey to Mastering Multi-Stage Builds
Bob's journey explores mastering multi-stage builds for leaner, secure Docker images. With Josie's advice and tools like Trivy, he learns to catch hidden vulnerabilities and strengthen his process. Follow along as Bob transforms his approach to container security.
Bob sat at his desk, watching the build logs run on his screen. He felt a pang of frustration. Despite diligently following the best practices he'd learned, his Docker images still seemed too large. He couldn't shake the nagging thought: "What am I missing?" Then, Josie's suggestion from last week echoed in his mind: "Have you tried multi-stage builds?" Bob's curiosity was piqued. He was ready to dive deeper.
đź“‚ Code Repository: Explore the complete code and configurations for this article on GitHub.
The Solution: Multi-Stage Builds
Josie's words played in his mind as he explored the idea. Multi-stage builds were a way to break down the build and runtime environments, streamlining the process to create smaller, more secure images.
"Think of it like prepping ingredients in separate kitchens and only carrying the essentials to the final dish" Bob thought.
Breaking It Down
Here's how Bob approached the solution:
- Stage 1: Build Environment – The heavy-lifting area where all the tools and build steps were used
- Stage 2: Runtime Environment – A minimalist setup where only the essentials made it through
He leaned forward, ready to tweak his Dockerfile.
# Stage 1: Build Environment
FROM node:22.9.0 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
Bob paused and smiled. This setup was his "workshop", where he did all the intense building work. But he knew he needed a clean, simple space for the final product to run.
# Stage 2: Runtime Environment
# Only essentials move forward to ensure minimal size.
FROM node:22.9.0-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
# Creating a non-root user to enhance security.
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["npm", "start"]
Bob's Implementation: Building and Running the Multi-Stage Build
Bob took a moment to reflect. "This is like moving the finished piece to the display room while leaving the mess behind" he thought as he set up the runtime stage. Only the dist
folder and essential files made it into this streamlined, minimal environment. He was now confident that his image size would shrink, reducing potential vulnerabilities.
After running his first multi-stage build, Bob realised the advantages went beyond just a smaller image:
- Smaller Images: Deployment was faster, and his company’s bandwidth usage decreased
- Enhanced Security: The reduced image size meant a smaller attack surface
- Improved Maintainability: His Dockerfile was now neatly organised and easier to manage
"It's like switching from a bulky toolbox to a sleek, portable kit" he mused, feeling a sense of accomplishment wash over him. Bob paused for a moment, realising how important it was to note down these key takeaways for future reference:
- Multi-stage builds streamline the build process, creating smaller and more efficient Docker images
- Using separate build and runtime environments helps minimise image size and reduces vulnerabilities
- The approach simplifies Dockerfiles, making them easier to maintain and audit
Reinforcing Trivy and Local Scanning
Bob recalled Josie's recommendation about using Trivy. "Don't just trust what looks good; dig deeper" she'd said. He wanted to reinforce this practice in the context of multi-stage builds. Running Trivy scans on both the build and runtime stages became his next task.
However, Bob faced an immediate challenge: scanning the build stage was not straightforward because intermediate images don't exist as standalone, nameable images by default. He learned that to scan a specific build stage, he needed to tag it as an image during the Docker build process.
After a few mins researching, Bob found a reference to the --target
flag in docker which allows him to build the interim stage build image so he can scan it so he ran:
$ docker build --target build -t bob:build .
...
...
$ docker image ls | grep bob
bob 1.0.0 81134bc2035b 4 minutes ago 153MB
bob build 8e39fa2f3ad1 4 minutes ago 1.13GB
Bob had always valued thoroughness, but until now, scanning his images felt like an optional extra rather than a critical step. Josie's words about digging deeper resonated with him as he set out to validate the security of his images. He started by scanning the runtime image, hoping for an all-clear.
$ trivy image bob:1.0.0
2024-11-02T13:48:15Z INFO [vuln] Vulnerability scanning is enabled
2024-11-02T13:48:15Z INFO [secret] Secret scanning is enabled
2024-11-02T13:48:15Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-11-02T13:48:15Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.56/docs/scanner/secret#recommendation for faster secret detection
2024-11-02T13:48:17Z INFO Detected OS family="alpine" version="3.20.3"
2024-11-02T13:48:17Z INFO [alpine] Detecting vulnerabilities... os_version="3.20" repository="3.20" pkg_num=16
2024-11-02T13:48:17Z INFO Number of language-specific files num=1
2024-11-02T13:48:17Z INFO [node-pkg] Detecting vulnerabilities...
2024-11-02T13:48:17Z WARN Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.56/docs/scanner/vulnerability#severity-selection for details.
bob:1.0.0 (alpine 3.20.3)
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
The results were exactly what he'd hoped for: no vulnerabilities found. Bob felt a surge of satisfaction ... his lean, streamlined runtime environment was clean. But he knew better than to rest easy. Josie's advice echoed in his mind: "This is where you catch what hides beneath the surface."
Determined, he moved on to scan the build image, where all the heavy lifting and dependencies resided.
$ trivy image bob:build
As the scan processed, Bob’s eyes widened at the sheer number of vulnerabilities displayed:
bob:build (debian 12.7)
=======================
Total: 1245 (UNKNOWN: 3, LOW: 528, MEDIUM: 567, HIGH: 140, CRITICAL: 7)
Bob sat back, feeling the weight of those numbers. It was overwhelming to see such a long list of potential issues lurking beneath what had seemed like a well-constructed build. He realised that while the runtime image was pristine, the build stage—the very foundation of his process—was riddled with risks.
At first, panic threatened to set in. But he caught himself. “Stay calm and dissect the problem,” he told himself, recalling Josie's encouragement. He needed to understand which of these vulnerabilities posed real threats and whether he could mitigate them without compromising functionality.
Bob jotted down a new list of priorities:
- Assess Critical Vulnerabilities: Review the highest-priority issues to understand their impact on his project
- Explore More Secure Base Images: Research alternatives, such as Alpine or distroless images, to reduce the inherent risks in his build
- Strengthen Routine Security Checks: Schedule regular scans for the build and runtime images, ensuring new vulnerabilities were caught early
The realisation was stark: securing his containers wasn't just about the final image but the whole process, starting from the build stage. Josie's advice had led him here, and now he was ready to push forward, transforming a daunting scan result into a roadmap for better practices.
Bob closed the scan results and leaned back in his chair, the initial dread replaced by resolve. "This is where I become better" he thought, ready to enhance his approach and fortify the security of his builds.
He spent the next few hours deep in research, evaluating the vulnerabilities and deciding on the best course of action. Bob discovered that while some issues could be mitigated with updates and patches, others required a more strategic approach, like switching to a leaner base image. He experimented with using Alpine and distroless images, testing and scanning them to compare results. Each scan taught him something new, revealing the trade-offs between security, functionality, and image size.
With each adjustment, Bob's understanding grew. He learned to balance a secure build environment with an efficient, lightweight runtime. He also set up a regular schedule for running Trivy scans and documented the steps needed to tackle new vulnerabilities as they arose. This wasn’t just about reacting ... it was about preparing for the future.
Finally, he ran one last scan on the updated build image.
$ trivy image bob:build-alpine
bob:build-alpine (alpine 3.20.3)
==============================
Total: 42 (UNKNOWN: 0, LOW: 13, MEDIUM: 21, HIGH: 7, CRITICAL: 1)
Bob nodded, satisfied. The vulnerabilities had significantly decreased, and the critical ones were manageable with immediate patching. His refined approach had paid off, reducing the risks while keeping his builds functional and secure.
The sun had dipped below the horizon, casting long shadows across his workspace. Bob glanced at the clock, surprised at how much time had passed. But he didn't mind. The initial wave of anxiety had transformed into a steady sense of accomplishment. His Docker builds were no longer just functional; they were becoming efficient and secure ... a testament to the effort he'd put into understanding the process from end to end.
Before shutting down for the day, he jotted down some more key takeaways:
- Running security scans on both the build and runtime images ensures vulnerabilities are caught early
- Tagging and scanning build stages with tools like Trivy is crucial for comprehensive image security
- Regular scanning and review of images help maintain security as dependencies change over time
Bob smiled, knowing that each lesson learned and applied brought him closer to mastering secure containerisation. With Josie's advice and his own persistence, Bob was now better equipped to handle the challenges that came with building and maintaining secure Docker images. And as he closed his laptop, he knew this was only the beginning of his next chapter.
"On to the next adventure" he thought, ready for whatever Docker might throw his way next.
Bob's journey demonstrates that securing Docker images goes beyond the initial build—it's an ongoing practice. By embracing multi-stage builds and integrating thorough scanning tools like Trivy, you can minimise vulnerabilities and maintain a lean, secure environment. As technology advances and new security challenges emerge, staying vigilant with regular scans, updates, and proactive measures remains essential. Now it's your turn—apply these lessons and enhance your own container security strategy, step by step.