5 Practical Docker Tips from the Captain’s Chair
Five Docker Captains share practical advice on hardened builds, SBOM attestations, debugging, and the AI-written Dockerfiles that keep getting shipped with root users.
Docker Captains spend their time solving container problems in very different environments: cloud platforms, front-end teams, Rails applications, open-source projects, and secure software delivery.
Docker's From the Captain's Chair series interviews different Captains about their work, and the resulting blog posts are a treasure trove of practical advice. This post re-reads the five available interviews through a security lens, highlighting the lessons that apply to hardened builds, SBOM attestations, debugging, and the AI-written Dockerfiles that keep getting shipped with root users.

So, let's dive into the five actionable tips.
P.S.: The Captains are more handsome than this AI-generated photo.
Key Takeaways
- AI-generated Dockerfiles can quietly ship a root user, hardcoded secrets, and a bloated image — review them like you would a pull request from a stranger.
- A strange-looking Dockerfile instruction is a signal to redesign, not to patch around — Docker Hardened Images remove entire categories of that strangeness.
- Multi-stage builds move compilers, package managers, and dev dependencies out of the image you ship, shrinking both the attack surface and the download.
- Generate the SBOM during the build and scan earlier stages so build-time dependencies remain visible after the final image discards them.
- Use Docker DX build debugging to understand failed builds with evidence instead of editing and rebuilding blind.
1. Review AI-Generated Dockerfiles Like You Would a Stranger's Pull Request
Kristiyan Velkov spends his days writing production Dockerfiles for front-end teams. When he describes his most recent "tricky technical challenge," it isn't a performance problem, it's a code review:
"A lot of developers were starting to use AI in our company, but I noticed some serious problems right away: images that were too large, broken caching, hardcoded environment variables, and containers running as root."
None of those are exotic failures. They are the default output of an agent that has never seen your threat model, optimized for "a Dockerfile that builds" rather than "a Dockerfile that's safe to run in production." An LLM has no reason to add a USER instruction unless you ask for one, and every reason to hardcode a value you pasted into the prompt. Checklist for anything an agent generates:
- Does the final stage run as a non-root
USER? - Are there secrets or tokens pasted directly into
ENVorARGinstead of injected at runtime? - Is the base image pinned, or did the agent pick whatever tag
latestresolved to that day?
Treat the output the way you'd treat a pull request from someone you've never met: useful starting point, not a merge candidate.
Perhaps, ask your AI agent:
Give me a Dockerfile that uses a hardened base image, runs as a non-root user, and generates an SBOM attestation for every stage, using the BuildKit's
BUILDKIT_SBOM_SCAN_STAGEmechanism.
Speaking of AI agents: a bad Dockerfile is what an agent writes. A separate, equally real problem is what an agent can reach while it's generating that Dockerfile in the first place: your filesystem, your credentials, your network. That's a different safeguard, and it's why SIP's first control is to run the agent itself inside a sandbox such as Docker Sandboxes (sbx), not to grant it the same access you have. Reviewing the output and containing the agent solve two different problems; skipping either one leaves a gap.
Check out sbx here:

Docker Sandbox Kits: A Hands-On Tutorial
A step-by-step walkthrough of Docker Sandboxes and Sandbox Kits: run a plain sandbox, compose a published Kit, then build your own GitHub CLI mixin.
containersecurity.dev
2. A Strange Dockerfile Instruction Means Stop, Not Patch
Igor Aleksandrov reviews a lot of Rails Dockerfiles, and his tip wasn't a tool, it was a heuristic:
"If something looks weird in your Dockerfile, you are doing it wrong."
He says, a good Dockerfile is a boring one. But, what if you want to make your Dockerfile secure? Then, it's complex and not boring anymore.
You want to:
- Add non-root users and
USERinstructions. - Remove shells and package managers.
- Strip out dev dependencies and build tools.
And, doing that, make it non-boring. Does it mean that Igor wants us to create insecure Dockerfiles?
There is software security rule that says: "Don't repeat yourself." If you need to add a USER instruction every time, something is wrong with the software design.
Docker, as a company cannot change the default behavior of a Dockerfile and make it non-root by default. But, Docker Hardened Images do that for you. They are a set of base images that are already hardened, so you don't have to do it yourself:
# For the build stage
FROM dhi.io/node:24-dev AS build
# For the production stage
FROM dhi.io/node:24
The production variant of the image is already non-root, and it has no shells or package managers. So, you can sit back and write a boring Dockerfile that is also secure. All you need to do is to swap FROM node for FROM dhi.io/node.
To learn more:

Docker Hardened Images are Free
Docker Hardened Images are now open-source under Apache 2.0 license and free to use in your projects.
containersecurity.dev
3. Multi-Stage Builds Are a Security Boundary, Not Just a Size Optimization
Pradumna Saraf gave the simplest tip of the five:
"Using multi-stage builds. It helps keep your images small, clean, secure, and production-ready. I have seen an image go from 1.7 GB to under 100 MB."
The size number is the headline, but "secure" is doing real work in that sentence. Compilers, source trees, package-manager caches, and node_modules/.m2 directories full of dev dependencies don't just make an image heavier, they give an attacker who gets a shell in the container more tools to work with, and they make the SBOM harder to reason about.
This is also SIP control iii, Harden Container Builds: separate the build environment from the runtime environment, then ship only the second one. The Dockerizing a Java 26 Project with Docker Init post walks through exactly that separation for a Spring Boot app: a JDK-based build stage, a slim JRE runtime stage, and nothing from the former leaking into the latter.

Dockerizing a Java 26 Project with Docker Init
Java 26 just landed. Here's how to Dockerize a Spring Boot project from scratch using Docker Init—the first move in the Docker Commandos playbook.
containersecurity.dev
Your dev dependencies should not bleed into your runtime image. Multi-stage builds are the mechanism that enforces that separation. But, this might cause a visibility issue: If you generate an SBOM after the build, it will only see the final stage, and omit all of the build-time dependencies. The next tip explains how to fix that.
4. Generate the SBOM During the Build, Not After
In my own Captain's Chair interview, my tip was to generate an SBOM attestation as part of the build itself, not as a separate step run against the finished image:
"You can generate SBOM attestation upon build very easily — it's just passing a flag on the CLI, setting a new argument in the CI job, or two lines of code if you're using Docker Bake."
docker buildx build --sbom=true -t <image> .
In a GitHub Actions workflow, that's again just a single line:
- name: Build and push image
uses: docker/build-push-action@v5
with:
push: true
sbom: true
tags: <image>
The reason this matters is structural, not stylistic. The final stage of the Dockerfile determines what actually ships, but the earlier stages determine how it was built. A dependency of an earlier stage does not end up on the final image. But if that dependency was malicious, it could have altered the build output in a way that survives into the final image.
The security risk is there, the SBOM evidence is not.
If you generate the SBOM after the fact, it will only see the final stage and omit all of the build-time dependencies. That's why the SBOM should be generated during the build, and why the earlier stages should be explicitly opted into the SBOM scan.
That's done through the BUILDKIT_SBOM_SCAN_STAGE argument:
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM dhi.io/node:24-dev AS build
It is documented in more depth in SIP control iv and in Generating SBOM with Docker Scout. The Poisoned Macchiato post goes one step further and shows why signing that embedded SBOM with Cosign:

The Poisoned Macchiato: Signing Java SBOMs with Cosign
A café, a suspicious espresso, and a lesson in supply chain trust. Learn how to generate BuildKit SBOM attestations for Java images and re-attest them with Cosign as OCI referrers.
containersecurity.dev
5. Debug Builds With Evidence, Not Trial and Error
Two Captains, in two separate interviews, named the same feature as their answer independently of each other. Naga Santhosh Reddy Vootukuri:
"With the current AI world we are living in, it's super easy to generate Dockerfiles, but the Docker DX VS Code extension makes it easy to live-debug to figure out any issues. This is a must-have tool in your arsenal."
And Igor Aleksandrov, describing the same feature as his current favorite Docker product, months earlier:
"As someone who spends a lot of time optimizing Rails Dockerfiles... debugging build failures used to be a real pain. [Build Debugging] has been a game-changer."
AI can produce a Dockerfile in seconds. It cannot guarantee the result caches correctly, resolves paths the way you expect, or fails for a reason you understand. When a build breaks, changing instructions at random produces a longer Dockerfile, not a better mental model of why it broke. A live debugger lets you inspect the actual filesystem state at the failing instruction — which is the same discipline Mini Shai-Hulud article argues for at the dependency level: Don't just make the error go away, understand what produced it.
One Workflow, Five Habits
These five tips reinforce each other, and they map onto the same chain SIP describes:
- Review what the agent generates before you trust it.
- Treat a strange instruction as a design smell, not a patch target.
- Separate build from runtime so only the runtime ships.
- Attach an inventory to what you ship, generated at build time.
- Debug with evidence when something breaks.
That's the real lesson from the Captain's Chair: the good Docker tips and the good security tips are usually the same tip, read from a different angle. Or perhaps not, and just because I'm a security person, I'm forcing the connection. :D
