<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: spmahapatra</title>
    <description>The latest articles on DEV Community by spmahapatra (@spmahapatra).</description>
    <link>https://dev.to/spmahapatra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2935765%2F32fa9f16-f43d-48ea-8148-249dd19c2b24.png</url>
      <title>DEV Community: spmahapatra</title>
      <link>https://dev.to/spmahapatra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spmahapatra"/>
    <language>en</language>
    <item>
      <title>Part 1.5: Optimizing Dockerfiles with Multi-Stage Builds</title>
      <dc:creator>spmahapatra</dc:creator>
      <pubDate>Sun, 20 Sep 2026 00:18:37 +0000</pubDate>
      <link>https://dev.to/spmahapatra/part-15-optimizing-dockerfiles-with-multi-stage-builds-m47</link>
      <guid>https://dev.to/spmahapatra/part-15-optimizing-dockerfiles-with-multi-stage-builds-m47</guid>
      <description>&lt;h1&gt;
  
  
  Part 1.5: Optimizing Dockerfiles with Multi-Stage Builds
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Platform engineers:&lt;/strong&gt; Reduce base image sizes by 80% to 95% while enforcing zero-toolchain runtime environments across build pipelines.&lt;br&gt;
&lt;strong&gt;SREs on-call:&lt;/strong&gt; Eliminate &lt;code&gt;KubeletHasDiskPressure&lt;/code&gt; node evictions and shorten deployment image pull times from minutes to seconds during autoscaling events.&lt;br&gt;
&lt;strong&gt;First-timers:&lt;/strong&gt; Start by separating your build-time SDKs from runtime binaries using named &lt;code&gt;FROM&lt;/code&gt; statements before tuning BuildKit caching mounts.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is Multi-Stage Docker Builds?
&lt;/h2&gt;

&lt;p&gt;Multi-stage Docker builds allow you to use multiple &lt;code&gt;FROM&lt;/code&gt; statements in a single &lt;code&gt;Dockerfile&lt;/code&gt;. Each &lt;code&gt;FROM&lt;/code&gt; instruction starts a new build stage with a fresh environment, letting you copy artifacts directly from one stage to another while dropping unnecessary compilers, source files, and temporary tooling.&lt;/p&gt;

&lt;p&gt;As of Docker 23.0 and BuildKit v0.11, multi-stage builds execute stages in parallel when dependencies permit, skipping unused stages entirely. Without multi-stage builds, shipping a Go, Rust, or Java application requires either carrying compiler toolchains directly into your execution environment or writing fragile host-level wrapper scripts to clean up files before layer commit.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;Why do single-stage Dockerfiles consistently inflate image size and expose unneeded system packages? Container images build as a series of read-only stackable layers. Deleting a tool in a later &lt;code&gt;RUN&lt;/code&gt; command does not reduce the storage footprint of the finished image; the layer containing that tool remains baked into the image history.&lt;/p&gt;

&lt;p&gt;Understanding where image bloat originates requires breaking down the container execution stack into distinct operational boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build Toolchain &amp;amp; Compiler Layer&lt;/strong&gt; — Compilers, C headers, git binaries, package managers (your build stage configuration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermediate Artifact Layer&lt;/strong&gt; — Compiled binaries, object files, dependency download caches (BuildKit engine / host engine storage).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Dependency Layer&lt;/strong&gt; — Shared dynamic libraries (&lt;code&gt;libc&lt;/code&gt;, &lt;code&gt;musl&lt;/code&gt;), CA certificates, system users (your final stage configuration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Runtime Engine Layer&lt;/strong&gt; — Namespace isolation, cgroups, &lt;code&gt;overlay2&lt;/code&gt; copy-on-write storage driver execution (host OS kernel, out of your hands).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You control layers 1, 2, and 3. Single-stage images force layers 1, 2, and 3 into the final layer stack. Multi-stage builds decouple layer 1 and layer 2 into throwaway stages, pulling only layer 3 into the final image definition.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;At 03:14 UTC, two nodes in our primary Kubernetes cluster threw &lt;code&gt;KubeletHasDiskPressure&lt;/code&gt; warnings. Within four minutes, three critical payment-gateway pods were evicted. Autoscaling attempted to launch replacement pods on surviving nodes, but the new pods stalled in &lt;code&gt;ContainerCreating&lt;/code&gt; for 240 seconds. &lt;/p&gt;

&lt;p&gt;The monitoring dashboard displayed the following event logs across the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03:18:12Z node-04 kubelet: WARNING: Disk usage on image filesystem is at 89%
03:19:01Z node-04 kubelet: ERROR: Failed to pull image "registry.internal/payment/api:v2.1.4": context deadline exceeded
03:19:05Z node-05 kubelet: INFO: Evicting pod payment-api-7db5c6c644-8x2ql due to disk pressure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why did image pulling take four minutes per node pull? I checked the image manifest in our internal registry and found the &lt;code&gt;payment/api:v2.1.4&lt;/code&gt; image measured 1.85 GB.&lt;/p&gt;

&lt;p&gt;I spent the first hour convinced our private registry proxy was bandwidth-throttled by cloud egress rules. It was not. I inspected the image layers using &lt;code&gt;docker history&lt;/code&gt; and discovered the build shipped the entire Go SDK, GCC compiler, standard header files, git repository history, and npm cache directly into runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATED BY                                      SIZE
/bin/sh -c apt-get update &amp;amp;&amp;amp; apt-get install…   642MB
/bin/sh -c go build -o /app/server .            480MB
COPY dir:a83b12... in /src                      720MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why were we shipping 1.84 GB of build infrastructure to serve a single 22 MB statically compiled binary? &lt;/p&gt;

&lt;p&gt;Honestly, I find Go's default static linking flags annoying because &lt;code&gt;-ldflags="-w -s"&lt;/code&gt; should be standard in every pipeline template, but engineers keep omitting it. The team had relied on a single-stage &lt;code&gt;Dockerfile&lt;/code&gt; based on &lt;code&gt;golang:1.21&lt;/code&gt; without stripping debug symbols or separating the build toolchain from the execution image.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker Engine 20.10.0+ or Docker Desktop 4.0+ with BuildKit enabled (&lt;code&gt;DOCKER_BUILDKIT=1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Basic familiarity with Docker CLI flags and container layer concepts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[VERIFY: docker buildx version]&lt;/code&gt; returns BuildKit engine active.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Audit image bloat and layer inventory
&lt;/h3&gt;

&lt;p&gt;Before rewriting your build definition, establish a baseline size and inspect which directives introduce unnecessary byte overhead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the legacy single-stage image&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; app:legacy &lt;span class="nt"&gt;-f&lt;/span&gt; Dockerfile.legacy &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Inspect total size&lt;/span&gt;
docker image &lt;span class="nb"&gt;ls &lt;/span&gt;app:legacy

&lt;span class="c"&gt;# Analyze layer breakdown&lt;/span&gt;
docker &lt;span class="nb"&gt;history &lt;/span&gt;app:legacy &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"table {{.ID}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.Size}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.CreatedBy}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confirm the total size and identify whether package managers (&lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;apk&lt;/code&gt;), compilers (&lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt;), or source trees account for more than 50% of the total footprint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect app:legacy &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.Size}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Isolate compilation dependencies using build stages
&lt;/h3&gt;

&lt;p&gt;Define a distinct compilation stage using a named &lt;code&gt;FROM&lt;/code&gt; clause. Allocate all heavy dependencies, header packages, and build scripts exclusively to this phase.&lt;/p&gt;

&lt;p&gt;Create a new &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build workspace&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency manifests first to leverage layer caching&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download

&lt;span class="c"&gt;# Copy source code and compile binary&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /build/bin/server .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verify that building only the intermediate stage succeeds without creating runtime side effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--target&lt;/span&gt; builder &lt;span class="nt"&gt;-t&lt;/span&gt; app:builder-only &lt;span class="nb"&gt;.&lt;/span&gt;
docker image &lt;span class="nb"&gt;ls &lt;/span&gt;app:builder-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Extract minimal binary targets into runtime images
&lt;/h3&gt;

&lt;p&gt;Add a second &lt;code&gt;FROM&lt;/code&gt; instruction targeting a minimal base image like &lt;code&gt;debian:bookworm-slim&lt;/code&gt;, &lt;code&gt;alpine&lt;/code&gt;, or &lt;code&gt;gcr.io/distroless/static-debian12&lt;/code&gt;. Copy only the compiled binary from the &lt;code&gt;builder&lt;/code&gt; stage using &lt;code&gt;COPY --from&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build workspace&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /build/bin/server .

&lt;span class="c"&gt;# Stage 2: Minimal Execution Runtime&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian12:nonroot&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy executable from builder stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/bin/server /app/server&lt;/span&gt;

&lt;span class="c"&gt;# Expose port and configure non-root runtime environment&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 65532:65532&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app/server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the new multi-stage build and compare the final image size against &lt;code&gt;app:legacy&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; app:optimized &lt;span class="nb"&gt;.&lt;/span&gt;
docker image &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"app&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;+(legacy|optimized)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output must show a dramatic reduction in size (for Go applications, usually dropping from ~1 GB to under 30 MB):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REPOSITORY   TAG         IMAGE ID       CREATED          SIZE
app          legacy      d3f2a1b4c5e6   10 minutes ago   1.85GB
app          optimized   a1b2c3d4e5f6   2 minutes ago    28.4MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Optimize BuildKit cache mounts across workflow stages
&lt;/h3&gt;

&lt;p&gt;While multi-stage builds shrink image size, re-downloading package caches on every build slows pipeline execution speed. Use BuildKit &lt;code&gt;--mount=type=cache&lt;/code&gt; options to persist dependency directories across stages without saving those directories into image layers.&lt;/p&gt;

&lt;p&gt;Modify the &lt;code&gt;builder&lt;/code&gt; stage in your &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;

&lt;span class="c"&gt;# Mount Go module cache and build cache directories&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go/pkg/mod &lt;span class="se"&gt;\
&lt;/span&gt;    go mod download

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go/pkg/mod &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/go-build &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /build/bin/server .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian12:nonroot&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/bin/server /app/server&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 65532:65532&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app/server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fast path — Build using BuildKit enabled inline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 docker build &lt;span class="nt"&gt;-t&lt;/span&gt; app:optimized-cached &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual path — build engine explicit export command if default context fails:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;-t&lt;/span&gt; app:optimized-cached &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Execute two sequential builds and check the time delta on the second run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; app:optimized-cached &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second run should complete in under two seconds due to BuildKit cache target hits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;p&gt;Selecting the right base image strategy for your final runtime stage introduces specific trade-offs between attack surface, debugging convenience, and POSIX compliance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Base Image Strategy&lt;/th&gt;
&lt;th&gt;Final Image Footprint&lt;/th&gt;
&lt;th&gt;Package Manager Available&lt;/th&gt;
&lt;th&gt;Vulnerability Exposure (CVEs)&lt;/th&gt;
&lt;th&gt;Debugging Capabilities&lt;/th&gt;
&lt;th&gt;Primary Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Full OS Base&lt;/strong&gt; (&lt;code&gt;debian:bookworm&lt;/code&gt;, &lt;code&gt;ubuntu:22.04&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;100 MB - 800 MB&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;apt&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;High (100+ standard packages)&lt;/td&gt;
&lt;td&gt;High (shell, curl, gdb present)&lt;/td&gt;
&lt;td&gt;Complex legacy applications needing dynamic C libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Minimal OS Base&lt;/strong&gt; (&lt;code&gt;alpine:3.19&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;5 MB - 15 MB&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;apk&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Low (fewer system packages)&lt;/td&gt;
&lt;td&gt;Moderate (busybox shell present)&lt;/td&gt;
&lt;td&gt;Applications requiring POSIX utilities but low disk usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Distroless&lt;/strong&gt; (&lt;code&gt;distroless/static-debian12&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2 MB - 20 MB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Minimal (no shell or package tools)&lt;/td&gt;
&lt;td&gt;Low (requires ephemeral debug containers)&lt;/td&gt;
&lt;td&gt;Security-critical services with static binaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Scratch&lt;/strong&gt; (&lt;code&gt;scratch&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0 MB (empty layer)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Zero OS-level CVEs&lt;/td&gt;
&lt;td&gt;None (no shell or tools)&lt;/td&gt;
&lt;td&gt;Purely static binaries (&lt;code&gt;Go&lt;/code&gt;, &lt;code&gt;Rust&lt;/code&gt;) with self-contained assets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Increasing security by stripping out shells (&lt;code&gt;distroless&lt;/code&gt;/&lt;code&gt;scratch&lt;/code&gt;) means your operational debugging workflow changes: you cannot &lt;code&gt;docker exec -it container sh&lt;/code&gt; into a failing instance. You must rely on telemetry, distributed tracing, or ephemeral debug sidecars instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Command Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Operational Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker build --target &amp;lt;stage_name&amp;gt; -t img .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build up to a specific intermediate stage for debugging or testing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker buildx build --cache-from type=gha --cache-to type=gha,mode=max .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable inline GitHub Actions caching for multi-stage BuildKit execution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker history --no-trunc &amp;lt;image_id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display full un-truncated layer commands to identify layer bloat source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker builder prune --filter type=exec.cachemount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clear persistent BuildKit cache mounts from local storage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t img .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Embed BuildKit cache metadata directly inside exported registry images.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Debugging &amp;amp; Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Missing CA certificates in &lt;code&gt;scratch&lt;/code&gt; base images
&lt;/h3&gt;

&lt;p&gt;When porting a Go or Rust application from &lt;code&gt;alpine&lt;/code&gt; or &lt;code&gt;debian&lt;/code&gt; to &lt;code&gt;scratch&lt;/code&gt;, outgoing HTTPS requests fail immediately upon startup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2024/03/15 10:22:41 Error fetching upstream API: Get "https://api.stripe.com/v1/charges": x509: certificate signed by unknown authority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;scratch&lt;/code&gt; image is entirely empty. It contains no root TLS certificates in &lt;code&gt;/etc/ssl/certs/ca-certificates.crt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Copy the certificate bundle from your builder stage into your final stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates update-ca-certificates

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/bin/server /app/server&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app/server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Invalidated layer caching from incorrect directive order
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;COPY&lt;/code&gt; or &lt;code&gt;RUN&lt;/code&gt; statement invalidates the cache for all subsequent lines if the source files change. Placing &lt;code&gt;COPY . .&lt;/code&gt; at the top of a multi-stage &lt;code&gt;Dockerfile&lt;/code&gt; causes &lt;code&gt;go mod download&lt;/code&gt; or &lt;code&gt;npm install&lt;/code&gt; to run on every line modification, ruining CI pipeline speed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# BAD: Invalidates package cache on any source file edit
COPY . .
RUN go mod download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separate dependency declarations from source files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# GOOD: Only re-downloads dependencies when manifests change&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dynamic C library resolution failure (&lt;code&gt;glibc&lt;/code&gt; vs &lt;code&gt;musl&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;A binary compiled on a standard Linux builder stage fails to execute in an Alpine runtime stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exec /app/server: no such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path &lt;code&gt;/app/server&lt;/code&gt; exists, but the dynamic linker loader specified inside the ELF binary header (typically &lt;code&gt;/lib64/ld-linux-x86-64.so.2&lt;/code&gt; from &lt;code&gt;glibc&lt;/code&gt;) is absent in Alpine (&lt;code&gt;musl&lt;/code&gt;-based).&lt;/p&gt;

&lt;p&gt;You have two choices to fix this execution error:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile a fully static binary in your build stage:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;   RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /app/server .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Switch your builder or runtime image so both use identical standard libraries (e.g., compile on &lt;code&gt;golang:alpine&lt;/code&gt; if deploying to &lt;code&gt;alpine&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Docker Documentation: &lt;a href="https://docs.docker.com/build/building/multi-stage/" rel="noopener noreferrer"&gt;Use multi-stage builds&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open Container Initiative: &lt;a href="https://github.com/opencontainers/image-spec" rel="noopener noreferrer"&gt;OCI Image Format Specification v1.0.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Container Tools: &lt;a href="https://github.com/GoogleContainerTools/distroless" rel="noopener noreferrer"&gt;Distroless Container Images Specification&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does BuildKit automatically skip unused build stages?
&lt;/h3&gt;

&lt;p&gt;BuildKit analyzes the target stage specified in your build invocation and builds a dependency graph. If a stage in your &lt;code&gt;Dockerfile&lt;/code&gt; is not referenced by the final stage or by a &lt;code&gt;COPY --from&lt;/code&gt; instruction, BuildKit completely skips the execution of that stage's commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I debug runtime errors when my target image lacks a shell?
&lt;/h3&gt;

&lt;p&gt;You cannot attach a bash shell to &lt;code&gt;scratch&lt;/code&gt; or &lt;code&gt;distroless&lt;/code&gt; containers directly. In Kubernetes 1.23+, use &lt;code&gt;kubectl debug&lt;/code&gt; to attach an ephemeral container containing diagnostic tools (&lt;code&gt;busybox&lt;/code&gt; or &lt;code&gt;nicolaka/netshoot&lt;/code&gt;) to the running pod's process namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl debug &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cgr.dev/chainguard/busybox &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;container-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local Docker testing, build a dedicated debug image target by adding a temporary stage ending in &lt;code&gt;FROM alpine&lt;/code&gt; or &lt;code&gt;FROM debian:slim&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I copy artifacts from an existing external Docker image instead of a local stage?
&lt;/h3&gt;

&lt;p&gt;Pass the target image directly into the &lt;code&gt;--from&lt;/code&gt; flag of a &lt;code&gt;COPY&lt;/code&gt; instruction. You do not need to write a pre-stage &lt;code&gt;FROM&lt;/code&gt; line to pull isolated assets from third-party tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy the official HashiCorp Vault binary directly into your runtime image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=hashicorp/vault:1.15.2 /bin/vault /usr/local/bin/vault&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Will multi-stage builds slow down CI/CD pipelines due to cache hits failing across ephemeral runners?
&lt;/h3&gt;

&lt;p&gt;Ephemeral CI runners start with fresh Docker daemon storage, causing local layer caches to miss on every pipeline run. Mitigate this by exporting inline BuildKit build caches directly to your container registry using &lt;code&gt;--cache-to=type=registry&lt;/code&gt; and &lt;code&gt;--cache-from=type=registry&lt;/code&gt; flags during the &lt;code&gt;docker buildx build&lt;/code&gt; execution step.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>Part 1 Dockerising and Standardising Compose</title>
      <dc:creator>spmahapatra</dc:creator>
      <pubDate>Sat, 19 Sep 2026 23:42:12 +0000</pubDate>
      <link>https://dev.to/spmahapatra/part-1-dockerising-and-standardising-compose-38gh</link>
      <guid>https://dev.to/spmahapatra/part-1-dockerising-and-standardising-compose-38gh</guid>
      <description>&lt;h1&gt;
  
  
  Part 1: Dockerising and Standardising Compose
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Navigation: &lt;a href="//./part-2-caching-api-calls-efficiently.md"&gt;Part 2: Caching API Calls Efficiently&lt;/a&gt; →&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start building a sample app along with use of different DevOps tools
&lt;/h2&gt;

&lt;p&gt;Welcome to the team! If you are a new developer joining us, this series is your definitive onboarding guide. We are taking a raw application and scaling it to enterprise standards.&lt;/p&gt;

&lt;p&gt;Here is our published journey:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose Standardisation&lt;/strong&gt; (This post) — Enforcing strict local environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Creation &amp;amp; Caching&lt;/strong&gt; (See Part 2) — Building the Flask app and using Redis to solve data latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise ELK Logging&lt;/strong&gt; (See Part 3) — Structured JSON logs for observability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-Cycle CI/CD&lt;/strong&gt; (See Part 4) — Shifting CI left to reduce costs.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Platform engineers:&lt;/strong&gt; Standardise Compose setups across your repositories. Use the Compose Specification schema. Remove the &lt;code&gt;version&lt;/code&gt; key and add a &lt;code&gt;name&lt;/code&gt; at the top level to stop cross-repo namespace clashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SREs on-call:&lt;/strong&gt; Stop silent cross-environment pollution and database corruption. This usually happens when local containers inherit shell exports instead of strict &lt;code&gt;.env&lt;/code&gt; fallbacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First-timers:&lt;/strong&gt; Start by splitting your configuration into two distinct layers: Data vs. Architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data (&lt;code&gt;.env&lt;/code&gt;)&lt;/strong&gt;: Injects raw strings and secrets (e.g. &lt;code&gt;DB_PASSWORD&lt;/code&gt;). Keep this developer-managed and &lt;em&gt;never&lt;/em&gt; commit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture (&lt;code&gt;docker-compose.override.yml&lt;/code&gt;)&lt;/strong&gt;: Overrides base container physics for local dev (mapping ports to your host, mounting live &lt;code&gt;./src&lt;/code&gt; folders). Keep this repo-managed and always commit it to version control.
Keep the base &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt; strictly for production-ready topologies.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is Dockerising and Standardising Compose?
&lt;/h2&gt;

&lt;p&gt;We often see teams using ad-hoc &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt; files. Standardising means we stop doing this. We replace them with a unified format using Compose V2. Instead of relying on custom wrapper scripts or raw &lt;code&gt;docker run&lt;/code&gt; commands, we agree on a strict structure for our services, volumes, and networks.&lt;/p&gt;

&lt;p&gt;As of July 2023, Docker Compose V1 (written in Python) reached End-of-Life (EOL). We must now rely on Compose V2 (written in Go), which natively reads the Compose Specification. &lt;strong&gt;Under this specification, the top-level &lt;code&gt;version&lt;/code&gt; is deprecated, and project identity is set by the root &lt;code&gt;name&lt;/code&gt; field, not the folder name.&lt;/strong&gt; If you skip this, you will face non-deterministic container naming and silent variable overrides across developer machines and CI/CD agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;When a developer runs &lt;code&gt;docker compose up&lt;/code&gt;, it doesn't just run a simple script. It parses and resolves your configuration through four layers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Repository Configuration&lt;/strong&gt; — &lt;code&gt;.env&lt;/code&gt; files, shell exports (what you control)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose Engine&lt;/strong&gt; — YAML parsing, variable expansion, inheritance (your local CLI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Engine API&lt;/strong&gt; — Translates YAML into API calls (your host daemon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host Kernel&lt;/strong&gt; — cgroup and network allocation (host OS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If two developers run &lt;code&gt;docker compose up&lt;/code&gt; on the same commit but get different results, where did it break? Not at the Engine or Kernel. It broke at the Repository Configuration layer. Unvalidated environment variables mutated the setup before a single API call reached the daemon.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;At 14:22 UTC, I received a critical page: &lt;code&gt;CRITICAL - DB_STAGING_MUTATION_ALERT&lt;/code&gt;. Staging database records were mutating, causing broken foreign keys.&lt;/p&gt;

&lt;p&gt;I checked the staging database audit logs immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt; &lt;span class="n"&gt;UTC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;18402&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;staging_db&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;STATEMENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local developer was running integration tests, thinking they were using a local database. Instead, their local app connected to our staging database over the corporate VPN and ran a destructive migration. &lt;/p&gt;

&lt;p&gt;Why did this happen? It was a variable cascade bleed. The developer's project relied on an implicit &lt;code&gt;.env&lt;/code&gt; file without a fallback for &lt;code&gt;DATABASE_HOST&lt;/code&gt;. Because the developer had previously exported &lt;code&gt;DATABASE_HOST=db-staging.internal.net&lt;/code&gt; in their shell session, the local &lt;code&gt;docker compose up&lt;/code&gt; silently inherited it.&lt;/p&gt;

&lt;p&gt;The trade-off is clear: fixing this requires tight environment file rules and explicit boundaries for every local stack. I spent two days clearing doubts with developers who insisted "it worked fine on my machine", before I finally set up a CI linter to reject unstandardised Compose files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Engine&lt;/strong&gt;: Version 24.0.0 or higher. Precheck your host version using &lt;code&gt;docker version --format '{{.Server.Version}}'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt;: Version 2.20.0 or higher (&lt;code&gt;docker compose version&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assumed Knowledge&lt;/strong&gt;: Familiarity with container networking and Linux processes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Precheck legacy configurations and remove deprecated attributes
&lt;/h3&gt;

&lt;p&gt;Inspect the repository for legacy Docker Compose formats. Older files use top-level &lt;code&gt;version&lt;/code&gt; keys (like &lt;code&gt;version: '3.8'&lt;/code&gt;). This ignores modern features.&lt;/p&gt;

&lt;p&gt;Run this command to find all YAML files with the deprecated &lt;code&gt;version&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 3 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"docker-compose*.yml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"docker-compose*.yaml"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Hn&lt;/span&gt; &lt;span class="s2"&gt;"^version:"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the &lt;code&gt;version&lt;/code&gt; field from all files. Replace it with an explicit &lt;code&gt;name&lt;/code&gt; attribute in your primary file. This ensures container and volume names are properly scoped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Standardised Compose Specification Format&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;core-platform-services&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${HOST_PORT:-8080}:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=${NODE_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=${DATABASE_URL:?Doubts: DATABASE_URL must be explicitly provided}&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal_net&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal_net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Validate that Compose parses the file without throwing schema errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Schema is valid."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Establish a multi-file split
&lt;/h3&gt;

&lt;p&gt;To prevent local settings from leaking into staging, separate the base topology from environment overrides. Use &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt; for shared definitions and &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.override.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.override.yml&lt;/code&gt;&lt;/a&gt; for developer-specific local mounts.&lt;/p&gt;

&lt;p&gt;Create the base &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-system&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.internal.net/billing/app:v2.4.1&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_PORT=3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app_bus&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the local development override file &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.override.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.override.yml&lt;/code&gt;&lt;/a&gt;. Docker Compose automatically merges this file when you run &lt;code&gt;docker compose up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/usr/src/app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verify how Compose merges these files locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the output contains both the base service definition and the local volume mounts.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Implement strict environment variable validation
&lt;/h3&gt;

&lt;p&gt;Unset shell variables can cause dangerous fallbacks. Use standard Compose syntax to enforce required variables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax &lt;code&gt;${VAR:-default}&lt;/code&gt; uses &lt;code&gt;default&lt;/code&gt; if &lt;code&gt;VAR&lt;/code&gt; is empty.&lt;/li&gt;
&lt;li&gt;Syntax &lt;code&gt;${VAR:?error_message}&lt;/code&gt; stops execution if &lt;code&gt;VAR&lt;/code&gt; is empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, create a sample &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/.env.example" rel="noopener noreferrer"&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/a&gt; file that developers can copy to &lt;code&gt;.env&lt;/code&gt;. This file must point to local dummy credentials, ensuring they never connect to staging by mistake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.example&lt;/span&gt;
&lt;span class="c"&gt;# Copy this file to .env before running docker compose up&lt;/span&gt;

&lt;span class="c"&gt;# Application Environment&lt;/span&gt;
&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development

&lt;span class="c"&gt;# Database Connection (Hardcoded to local dummy container, NOT staging)&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1
&lt;span class="nv"&gt;DATABASE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5432
&lt;span class="nv"&gt;DATABASE_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local_dummy_user
&lt;span class="nv"&gt;DATABASE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local_dummy_pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, enforce required variable checks inside your services block in &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass ${REDIS_PASSWORD:?Fatal error&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REDIS_PASSWORD is missing.}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Test that Compose throws an explicit error when executing without required variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;You can see a complete, working implementation of these standards in our &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm" rel="noopener noreferrer"&gt;Rick &amp;amp; Morty API repository&lt;/a&gt;. &lt;br&gt;
Check out the split between &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.override.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.override.yml&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/.env.example" rel="noopener noreferrer"&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/a&gt; to see how we safely expose local ports without compromising the base production configuration.&lt;/p&gt;


&lt;h2&gt;
  
  
  Debugging &amp;amp; Common Pitfalls
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Silent Environment Variable Overrides
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Services connect to unintended host ports or external services, bypassing &lt;code&gt;.env&lt;/code&gt; values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root Cause:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Compose resolves variables using this precedence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shell variables set in the active terminal&lt;/li&gt;
&lt;li&gt;Environment variables set in &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Default values in the &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a developer exports &lt;code&gt;PORT=9000&lt;/code&gt; in their shell, Compose will use &lt;code&gt;9000&lt;/code&gt;, silently ignoring &lt;code&gt;PORT=3000&lt;/code&gt; inside &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Clear ambient environment variables before invoking Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Volume Namespace Bleed Across Repositories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Spinning up an application in Repository B overwrites persistent data in database volumes created by Repository A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root Cause:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Both repositories define generic volume keys like &lt;code&gt;volumes: db_data:&lt;/code&gt; without setting a root &lt;code&gt;name&lt;/code&gt; attribute. Compose defaults to the current parent directory name. If both projects are inside directories named &lt;code&gt;app/&lt;/code&gt;, they will share the exact same volume namespace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Explicitly assign a unique top-level &lt;code&gt;name&lt;/code&gt; in every repository's &lt;a href="https://github.com/spmahapatra/rick-morty-api-helm/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why should I delete the top-level &lt;code&gt;version&lt;/code&gt; line?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; field enforced legacy schema constraints (like &lt;code&gt;'3.8'&lt;/code&gt;). Modern Compose V2 automatically detects capabilities based on the target Docker Engine. Including a &lt;code&gt;version&lt;/code&gt; string now triggers deprecation warnings and can cause Compose to ignore modern attributes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run &lt;code&gt;docker-compose&lt;/code&gt; (with a hyphen) in modern CI pipelines?
&lt;/h3&gt;

&lt;p&gt;You should not rely on &lt;code&gt;docker-compose&lt;/code&gt; anymore. The standalone Python binary reached End-of-Life in July 2023. Modern pipelines must use &lt;code&gt;docker compose&lt;/code&gt; (space separated), which uses the Go-based plugin integrated into the Docker CLI.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>engineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Installing Minikube on Debian</title>
      <dc:creator>spmahapatra</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:59:01 +0000</pubDate>
      <link>https://dev.to/spmahapatra/installing-minikube-on-debian-1aj4</link>
      <guid>https://dev.to/spmahapatra/installing-minikube-on-debian-1aj4</guid>
      <description>&lt;h1&gt;
  
  
  Installing Minikube on Debian
&lt;/h1&gt;

&lt;p&gt;Minikube is a practical way to run a small Kubernetes cluster on a Debian workstation or development instance. This guide installs Minikube with the Docker driver, starts a named profile, runs a temporary deployment, and removes the test resources afterward.&lt;/p&gt;

&lt;p&gt;This is a local development setup, not a production Kubernetes distribution. The commands target Debian 12 (Bookworm), Debian 13 (Trixie), or a compatible newer Debian release on an x86-64 host.&lt;/p&gt;

&lt;p&gt;The goal is a repeatable local baseline rather than a particular Minikube release number. Minikube and Kubernetes change over time, so the release documentation remains the authority for supported versions and driver behavior. The commands deliberately verify each boundary: the Debian host, Docker, the Minikube profile, the Kubernetes node, and a real workload. That makes a later failure easier to classify. For example, a failed &lt;code&gt;docker run&lt;/code&gt; is a runtime problem, while a successful Docker test followed by a failed rollout belongs in the cluster or workload layer.&lt;/p&gt;

&lt;p&gt;Run the commands as the same non-root user who will use Minikube afterward. Mixing &lt;code&gt;sudo minikube&lt;/code&gt; with normal-user commands creates separate configuration and cache locations, which can make a healthy cluster appear to be missing. Keeping one user and one named profile throughout the walkthrough avoids that confusing split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You will have a Docker-backed Minikube profile named &lt;code&gt;local-kube-cluster&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reserve at least 2 CPUs, 4 GiB of memory, and 20 GiB of free disk space for a comfortable first run.&lt;/li&gt;
&lt;li&gt;The Docker driver avoids a second virtual machine, but it still needs a working Docker daemon and permission to access it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is Minikube?
&lt;/h2&gt;

&lt;p&gt;Minikube runs a single-node Kubernetes cluster locally so you can develop manifests, test controllers, and follow tutorials without provisioning a remote cluster. The Docker driver runs the node inside a container managed by Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The driver is the important choice:&lt;/strong&gt; Minikube manages the Kubernetes node, while Docker provides the container runtime and host-level isolation. If either layer is unhealthy, Kubernetes startup errors can be misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;Think of the installation as four independent checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host capacity:&lt;/strong&gt; the machine has enough CPU, memory, disk, and network access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container runtime:&lt;/strong&gt; Docker is installed, running, and usable by the current user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster profile:&lt;/strong&gt; Minikube creates and stores a named cluster configuration. A profile can retain a driver choice and resource settings from an earlier attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes workload:&lt;/strong&gt; a &lt;code&gt;Ready&lt;/code&gt; node only proves the control plane started. A deployment and service smoke test proves that the cluster can schedule a workload and expose it through the selected driver.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This model also explains the safest troubleshooting order: check the host first, then Docker, then the Minikube profile, and only then the Kubernetes workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian 12, Debian 13, or a compatible newer Debian release&lt;/li&gt;
&lt;li&gt;x86-64 Linux, unless you download the matching Minikube architecture binary&lt;/li&gt;
&lt;li&gt;A non-root user with &lt;code&gt;sudo&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;At least 2 CPUs, 4 GiB RAM, and 20 GiB of free disk space&lt;/li&gt;
&lt;li&gt;Outbound access to Debian, Docker, Kubernetes, and Minikube download endpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd&lt;/code&gt; available if you want Docker managed as a system service&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Confirm the Debian host has enough capacity
&lt;/h3&gt;

&lt;p&gt;Update the package index, install the utilities used by the setup, and inspect the resources before downloading anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg lsb-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;nproc
&lt;/span&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;sudo df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not continue with the default profile if the host has fewer than 2 CPUs or less than 4 GiB of usable memory. A cluster can technically start with less, but image pulls and system pods will compete for the same constrained resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install and verify Docker Engine
&lt;/h3&gt;

&lt;p&gt;Before continuing, install Docker Engine by following the dedicated &lt;a href="https://spmahapatra.github.io/install-docker-engine-on-debian/" rel="noopener noreferrer"&gt;Install Docker Engine on Debian&lt;/a&gt; guide. It uses Docker's official APT repository and covers conflicting packages, repository key configuration, daemon access, firewall considerations, and cleanup.&lt;/p&gt;

&lt;p&gt;After Docker is installed, verify it as the same user who will run Minikube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl is-active docker
docker version
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;docker version&lt;/code&gt; reports a permission error, follow the non-root access and new-login-session instructions in the Docker guide. The Docker group grants root-equivalent control over the host, so only trusted users should receive this access.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Install the Minikube binary
&lt;/h3&gt;

&lt;p&gt;Download the latest stable x86-64 Linux binary from Minikube's official release location and install it in &lt;code&gt;/usr/local/bin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 minikube-linux-amd64 /usr/local/bin/minikube
&lt;span class="nb"&gt;rm &lt;/span&gt;minikube-linux-amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation and make Docker the default driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube version
minikube config &lt;span class="nb"&gt;set &lt;/span&gt;driver docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For ARM64, download the matching binary from the &lt;a href="https://github.com/kubernetes/minikube/releases/latest" rel="noopener noreferrer"&gt;Minikube releases page&lt;/a&gt; instead of the x86-64 file above.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Install kubectl locally
&lt;/h3&gt;

&lt;p&gt;Install the latest stable x86-64 &lt;code&gt;kubectl&lt;/code&gt; binary, verify its SHA-256 checksum, and move it into &lt;code&gt;/usr/local/bin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;KUBECTL_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://dl.k8s.io/release/stable.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; &lt;span class="s2"&gt;"https://dl.k8s.io/release/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUBECTL_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/linux/amd64/kubectl"&lt;/span&gt;
curl &lt;span class="nt"&gt;-fLO&lt;/span&gt; &lt;span class="s2"&gt;"https://dl.k8s.io/release/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUBECTL_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/linux/amd64/kubectl.sha256"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;kubectl.sha256&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;  kubectl"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 kubectl /usr/local/bin/kubectl
&lt;span class="nb"&gt;rm &lt;/span&gt;kubectl kubectl.sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the local client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands above target x86-64 Linux. For an ARM64 host, replace &lt;code&gt;amd64&lt;/code&gt; in both download URLs with &lt;code&gt;arm64&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep the client and cluster lifecycle separate. &lt;code&gt;kubectl&lt;/code&gt; is the command-line client installed on the Debian host, while Minikube owns the local cluster and its certificates. Starting the profile writes or updates a context in the current user's kubeconfig. After activating &lt;code&gt;local-kube-cluster&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt; uses that active context by default. Run &lt;code&gt;kubectl config get-contexts&lt;/code&gt; if you need to inspect available contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Start a Docker-backed cluster profile
&lt;/h3&gt;

&lt;p&gt;Create a named profile with explicit resources. Naming the profile makes it possible to inspect, stop, and delete this cluster without affecting another Minikube profile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the named profile active, then check both the Minikube state and the Kubernetes node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube profile local-kube-cluster
minikube status &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
minikube profile
kubectl config current-context
kubectl get nodes
kubectl get pods &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minikube profile&lt;/code&gt; command should now print &lt;code&gt;local-kube-cluster&lt;/code&gt;, and &lt;code&gt;kubectl config current-context&lt;/code&gt; should show the same context. The profile should report &lt;code&gt;Running&lt;/code&gt;, one node should report &lt;code&gt;Ready&lt;/code&gt;, and system pods may need a short time to settle during the first image pull.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Run and remove a Kubernetes smoke test
&lt;/h3&gt;

&lt;p&gt;Create a temporary deployment, expose it as a NodePort service, and wait for the rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create deployment minikube-smoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kicbase/echo-server:1.0
kubectl expose deployment minikube-smoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NodePort &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8080
kubectl rollout status deployment/minikube-smoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
kubectl get deployment,service,pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask Minikube for a URL to the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube service minikube-smoke &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster &lt;span class="nt"&gt;--url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the temporary objects after the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete service minikube-smoke
kubectl delete deployment minikube-smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Debugging &amp;amp; Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker permission denied
&lt;/h3&gt;

&lt;p&gt;If Docker works with &lt;code&gt;sudo&lt;/code&gt; but not as your normal user, the current shell has not picked up the new group membership. Run &lt;code&gt;groups&lt;/code&gt; and &lt;code&gt;id -nG&lt;/code&gt;, start a new login session, and retry &lt;code&gt;docker run --rm hello-world&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insufficient resources
&lt;/h3&gt;

&lt;p&gt;Check &lt;code&gt;free -h&lt;/code&gt;, &lt;code&gt;nproc&lt;/code&gt;, and &lt;code&gt;df -h /&lt;/code&gt;. Stop competing workloads or increase the instance size before changing Kubernetes settings. Reducing memory can make the cluster appear to start while leaving system pods unable to schedule reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Profile uses the wrong driver
&lt;/h3&gt;

&lt;p&gt;Profiles retain configuration. Inspect existing profiles and recreate the named profile if it was previously started with another driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube profile list
minikube delete &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
minikube start &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096 &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Startup fails behind a proxy
&lt;/h3&gt;

&lt;p&gt;Image pulls and package downloads need network access from both the host and Docker. Configure the proxy according to your environment, then inspect the profile diagnostics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube logs &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
minikube status &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A failed start leaves stale resources
&lt;/h3&gt;

&lt;p&gt;Delete the profile and retry only after checking the logs. Repeating &lt;code&gt;minikube start&lt;/code&gt; without removing a partially created profile can preserve the original driver or resource settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Minikube suitable for production?
&lt;/h3&gt;

&lt;p&gt;No. Minikube is designed for local development, learning, and repeatable experiments. Use a production-oriented Kubernetes distribution or managed service when you need high availability, durable operations, and multi-node failure handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why install standalone kubectl?
&lt;/h3&gt;

&lt;p&gt;The standalone client provides the normal Kubernetes workflow and can switch among contexts with &lt;code&gt;kubectl config get-contexts&lt;/code&gt;. Installing it once also keeps the commands in this guide consistent with other Kubernetes tools and scripts. Minikube still manages the cluster and writes the &lt;code&gt;local-kube-cluster&lt;/code&gt; context into your kubeconfig.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use the Docker driver on Debian?
&lt;/h3&gt;

&lt;p&gt;It is usually the simplest option when Docker is already part of the development workflow. It avoids managing a second virtual machine, though it still consumes host CPU, memory, disk, and Docker daemon resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I do when the profile is no longer needed?
&lt;/h3&gt;

&lt;p&gt;Stop it to retain its downloaded data and configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube stop &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete it to remove the profile and its resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube delete &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local-kube-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>kubernetes</category>
      <category>minikube</category>
      <category>debian</category>
      <category>docker</category>
    </item>
    <item>
      <title>Install Docker Engine on Debian</title>
      <dc:creator>spmahapatra</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:59:00 +0000</pubDate>
      <link>https://dev.to/spmahapatra/install-docker-engine-on-debian-4dga</link>
      <guid>https://dev.to/spmahapatra/install-docker-engine-on-debian-4dga</guid>
      <description>&lt;h1&gt;
  
  
  Install Docker Engine on Debian
&lt;/h1&gt;

&lt;p&gt;Docker Engine provides the daemon, CLI, and runtime needed to build and run containers on Debian. This guide installs Docker Engine from Docker's official APT repository, verifies the service with a test container, and configures the current user to run Docker without &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The repository method is preferable for a long-lived Debian instance because APT can receive Docker updates through the normal package workflow. Docker currently documents Debian 13 (Trixie), Debian 12 (Bookworm), and Debian 11 (Bullseye), with packages for amd64, armhf, arm64, and ppc64el. Package names and supported releases can change, so the official &lt;a href="https://docs.docker.com/engine/install/debian/" rel="noopener noreferrer"&gt;Docker Debian installation page&lt;/a&gt; remains the authority when this article is updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install Docker Engine from Docker's signed APT repository rather than mixing distribution packages with Docker packages.&lt;/li&gt;
&lt;li&gt;Test the daemon with &lt;code&gt;docker run hello-world&lt;/code&gt; before configuring applications or Kubernetes tools.&lt;/li&gt;
&lt;li&gt;Membership in the &lt;code&gt;docker&lt;/code&gt; group grants root-equivalent access to the host. Add only trusted users, or keep using &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is Docker Engine?
&lt;/h2&gt;

&lt;p&gt;Docker Engine is the host-side service that builds images, stores container data, creates networks, and starts containers. The Docker CLI sends requests to that service through its local socket. Installing the CLI alone does not provide a working container runtime.&lt;/p&gt;

&lt;p&gt;The installation has three separate concerns: repository trust, package installation, and daemon access. The repository's signing key lets APT verify Docker packages. The packages install the daemon and related plugins. The Unix socket controls which users can ask that daemon to perform privileged operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;Treat the setup as a chain of checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Operating system:&lt;/strong&gt; confirm the Debian release and architecture are supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflicting packages:&lt;/strong&gt; remove unofficial packages that can provide overlapping commands or incompatible runtime dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APT trust:&lt;/strong&gt; install Docker's keyring and repository definition with readable, explicit permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine service:&lt;/strong&gt; install the daemon, CLI, containerd, Buildx, and Compose plugins, then confirm the service is active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User access:&lt;/strong&gt; decide whether commands should use &lt;code&gt;sudo&lt;/code&gt;, Docker-group membership, or rootless mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network policy:&lt;/strong&gt; review firewall behavior before publishing container ports.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A successful &lt;code&gt;docker version&lt;/code&gt; proves that the CLI can reach the daemon. A successful &lt;code&gt;docker run hello-world&lt;/code&gt; proves that the daemon can pull an image, create a container, start it, and report its output. Run both checks; they catch different failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;On a new Debian instance, I once treated Docker installation as a single package command and moved directly to a Kubernetes setup. The package installation completed, but the normal user could not access the Docker socket, so Minikube reported a driver failure that looked like a Kubernetes problem. I spent time checking cluster settings before checking &lt;code&gt;docker run&lt;/code&gt;. The real fix was a new login session after changing group membership. The lesson was simple: validate Docker as the exact user and shell that will run the next tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian 11, 12, or 13 on a supported architecture&lt;/li&gt;
&lt;li&gt;A non-root user with &lt;code&gt;sudo&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;Outbound HTTPS access to Debian mirrors and &lt;code&gt;download.docker.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A terminal session on the target Debian instance&lt;/li&gt;
&lt;li&gt;A firewall plan if containers will publish ports outside the host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before installing, identify the release and architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Debian release: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Codename: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Remove conflicting Docker packages
&lt;/h3&gt;

&lt;p&gt;Debian may provide packages such as &lt;code&gt;docker.io&lt;/code&gt;, while other tools may install &lt;code&gt;containerd&lt;/code&gt; or &lt;code&gt;runc&lt;/code&gt; separately. Docker's official packages bundle compatible runtime dependencies. Remove conflicting packages if they are present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove &lt;span class="nt"&gt;-y&lt;/span&gt; docker.io docker-compose docker-doc docker-buildx podman-docker containerd runc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT may report that some packages are not installed. This command does not remove Docker's stored images, containers, volumes, or networks. Existing data may still be present under &lt;code&gt;/var/lib/docker&lt;/code&gt; and &lt;code&gt;/var/lib/containerd&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure Docker's signed APT repository
&lt;/h3&gt;

&lt;p&gt;Install the keyring directory and Docker's official signing key, then create a deb822 repository definition. The architecture expression prevents APT from selecting packages for a different architecture.&lt;/p&gt;

&lt;p&gt;If Docker was previously configured with an older &lt;code&gt;.list&lt;/code&gt; file, remove that duplicate source before adding the &lt;code&gt;.sources&lt;/code&gt; definition below. Keeping two entries for the same repository with different signing keys makes APT stop with a &lt;code&gt;Conflicting values set for option Signed-By&lt;/code&gt; error. This removes repository configuration only; it does not remove Docker packages or container data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/sources.list.d/docker.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/debian/gpg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Components: stable
Architectures: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh package metadata and confirm Docker packages are visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
apt list &lt;span class="nt"&gt;--all-versions&lt;/span&gt; docker-ce 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,5p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Debian testing or a derivative distribution, &lt;code&gt;VERSION_CODENAME&lt;/code&gt; may not match a Docker repository suite. Check the official documentation before substituting the corresponding Debian codename.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Install Docker Engine and its standard plugins
&lt;/h3&gt;

&lt;p&gt;Install the engine, command-line client, containerd, Buildx, and Compose plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
  docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the service is active and that the client can reach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl is-active docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker version
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a normal Debian installation, Docker starts with the service installation. If it is inactive, start it and inspect the service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Verify the installation with a test container
&lt;/h3&gt;

&lt;p&gt;Run Docker's small verification image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command downloads the image if necessary, creates a temporary container, prints a confirmation message, and removes the container because of &lt;code&gt;--rm&lt;/code&gt;. A failure here should be resolved before installing Minikube, Compose applications, or other Docker-dependent tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Configure non-root Docker access deliberately
&lt;/h3&gt;

&lt;p&gt;The simplest access model is to prefix Docker commands with &lt;code&gt;sudo&lt;/code&gt;. If the instance is trusted and the workflow requires normal-user commands, add the current user to the Docker group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a new login session so the shell receives the new group membership. Then verify the exact user path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-nG&lt;/span&gt;
docker version
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use &lt;code&gt;sudo docker&lt;/code&gt; as a substitute for refreshing the session when testing group access. The root and non-root clients can use different configuration files and caches, which makes troubleshooting harder.&lt;/p&gt;

&lt;p&gt;The Docker group is not equivalent to an ordinary application group. A user who can control the Docker daemon can generally gain root-level access to the host. Use the group only for trusted administrators, or investigate Docker's documented rootless mode when that privilege boundary matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Verify Compose and Buildx
&lt;/h3&gt;

&lt;p&gt;The installation includes the modern Compose and Buildx plugins. Confirm both are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
docker buildx version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are plugin commands, so &lt;code&gt;docker-compose&lt;/code&gt; and an old standalone &lt;code&gt;docker-buildx&lt;/code&gt; command are not required for the documented workflow. Existing scripts may need updating if they depend on legacy command names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Docker Daily
&lt;/h2&gt;

&lt;p&gt;Useful commands for a new instance include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List running containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List running and stopped containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List locally stored images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker logs &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read container output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker exec -it &amp;lt;container&amp;gt; sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open a shell in a running container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker inspect &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show low-level container configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker system df&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show Docker disk usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a Compose application in the background&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep the daemon healthy with &lt;code&gt;systemctl is-active docker&lt;/code&gt; and monitor disk usage. Images, writable layers, build cache, and container logs can fill a small instance even when few containers are running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging &amp;amp; Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;docker: permission denied&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The daemon may be healthy while the current user lacks permission to access &lt;code&gt;/var/run/docker.sock&lt;/code&gt;. Check the service, socket ownership, and active groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl is-active docker
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/run/docker.sock
&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-nG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;sudo docker ...&lt;/code&gt;, or start a new login session after &lt;code&gt;usermod -aG docker "$USER"&lt;/code&gt;. Avoid changing the socket to world-writable permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  APT cannot find &lt;code&gt;docker-ce&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The Docker repository may be missing, the codename may be wrong, or &lt;code&gt;sudo apt update&lt;/code&gt; may have failed. Inspect the source definition and run the update again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/sources.list.d/docker.sources
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On supported Debian releases, the &lt;code&gt;Suites&lt;/code&gt; value should correspond to the Debian codename. Do not blindly use &lt;code&gt;stable&lt;/code&gt; as the suite; &lt;code&gt;stable&lt;/code&gt; belongs in &lt;code&gt;Components&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker service fails to start
&lt;/h3&gt;

&lt;p&gt;Read the service log before reinstalling packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; docker.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for storage-driver, dependency, disk, or network errors. A previous installation may have left incompatible runtime packages or configuration under &lt;code&gt;/etc/docker&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Published ports bypass expected firewall rules
&lt;/h3&gt;

&lt;p&gt;Docker warns that ports published with &lt;code&gt;-p&lt;/code&gt; can bypass rules managed by &lt;code&gt;ufw&lt;/code&gt; or firewalld. Docker is compatible with &lt;code&gt;iptables-nft&lt;/code&gt; and &lt;code&gt;iptables-legacy&lt;/code&gt;; rules created only with unsupported native nftables workflows may not behave as expected. Review Docker's firewall documentation and place filtering rules in the &lt;code&gt;DOCKER-USER&lt;/code&gt; chain where appropriate before exposing services.&lt;/p&gt;

&lt;h3&gt;
  
  
  The server runs out of disk
&lt;/h3&gt;

&lt;p&gt;Inspect Docker's usage before deleting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker system &lt;span class="nb"&gt;df
sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /var/lib/docker /var/lib/containerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove only resources you understand. &lt;code&gt;docker system prune&lt;/code&gt; can remove stopped containers, unused networks, dangling images, and build cache; adding &lt;code&gt;--volumes&lt;/code&gt; can remove unused data volumes and should be treated as a destructive operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I install &lt;code&gt;docker.io&lt;/code&gt; or &lt;code&gt;docker-ce&lt;/code&gt; on Debian?
&lt;/h3&gt;

&lt;p&gt;Use Docker's official packages when you want Docker's documented Engine release stream: &lt;code&gt;docker-ce&lt;/code&gt;, &lt;code&gt;docker-ce-cli&lt;/code&gt;, &lt;code&gt;containerd.io&lt;/code&gt;, &lt;code&gt;docker-buildx-plugin&lt;/code&gt;, and &lt;code&gt;docker-compose-plugin&lt;/code&gt;. Do not mix the distribution's &lt;code&gt;docker.io&lt;/code&gt; package with those packages without checking the resulting dependency and upgrade behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run Docker without &lt;code&gt;sudo&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Yes, but adding a user to the Docker group grants root-equivalent control over the host. A new login session is required after changing membership. Keeping &lt;code&gt;sudo&lt;/code&gt; is simpler for a tightly controlled administrative workflow; rootless mode is another option when its limitations fit the workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does installing Docker automatically start the daemon?
&lt;/h3&gt;

&lt;p&gt;Docker's Debian package normally starts the service. Verify with &lt;code&gt;sudo systemctl is-active docker&lt;/code&gt;, and use &lt;code&gt;sudo systemctl enable --now docker&lt;/code&gt; if the service is inactive or is not enabled for boot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the convenience script better than the APT repository?
&lt;/h3&gt;

&lt;p&gt;The convenience script is useful for disposable development environments and automation experiments, but it provides less control over repository setup and package choices. The APT repository is the better default for a Debian instance that will be maintained over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I completely uninstall Docker?
&lt;/h3&gt;

&lt;p&gt;First remove the packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
  docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then remove the repository definition and key if they are no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/sources.list.d/docker.sources
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Images, containers, volumes, and custom configuration are not automatically removed by package removal. Delete &lt;code&gt;/var/lib/docker&lt;/code&gt; and &lt;code&gt;/var/lib/containerd&lt;/code&gt; only after confirming that all required data has been backed up or discarded.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>debian</category>
      <category>containers</category>
      <category>linux</category>
    </item>
    <item>
      <title>Installing Debian on WSL2 in Windows 11</title>
      <dc:creator>spmahapatra</dc:creator>
      <pubDate>Sat, 25 Apr 2026 23:14:06 +0000</pubDate>
      <link>https://dev.to/spmahapatra/how-to-set-up-wsl2-on-windows-11-53oi</link>
      <guid>https://dev.to/spmahapatra/how-to-set-up-wsl2-on-windows-11-53oi</guid>
      <description>&lt;h1&gt;
  
  
  Installing Debian on WSL2 in Windows 11
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;WSL2 with Debian gives you a full Linux kernel and systemd support — a legitimate CI parity environment on Windows hardware without running a separate VM.&lt;/li&gt;
&lt;li&gt;Take a snapshot with &lt;code&gt;wsl --export&lt;/code&gt; before installing any project tooling (Step 7). It's a one-minute operation that has saved me multiple full reinstalls.&lt;/li&gt;
&lt;li&gt;Steps 1–5 get you a working Debian environment with systemd. Steps 6–7 (Windows Terminal, snapshot) are worth doing the same session.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is WSL2?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WSL2 (Windows Subsystem for Linux 2) ships a real Linux kernel inside a lightweight Hyper-V virtual machine.&lt;/strong&gt; Unlike WSL1, which translated Linux syscalls into Windows calls, WSL2 runs an actual Linux kernel — meaning full syscall compatibility, &lt;code&gt;systemd&lt;/code&gt;, and &lt;code&gt;eBPF&lt;/code&gt; all work without a separate VM.&lt;/p&gt;

&lt;p&gt;As of Windows 11 22H2, WSL2 supports &lt;code&gt;systemd&lt;/code&gt; natively with no workarounds, persistent background services, and GPU passthrough via CUDA on supported hardware. This makes it the first Windows-native environment where you can run production-parity workloads alongside your Windows desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;WSL2 runs inside a single lightweight Hyper-V VM (the "WSL2 utility VM"). Each installed distro is a separate filesystem image (&lt;code&gt;ext4.vhdx&lt;/code&gt;) running inside that VM. The VM boots once; all distros share the kernel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Windows filesystem (&lt;code&gt;C:\&lt;/code&gt;) is mounted at &lt;code&gt;/mnt/c/&lt;/code&gt;&lt;/strong&gt; — I/O across this boundary is slow. Keep project files in &lt;code&gt;~/&lt;/code&gt; (inside the distro), not in &lt;code&gt;/mnt/c/Users/...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wsl.exe&lt;/code&gt; is your control plane&lt;/strong&gt;: install, unregister, export, import, and configure distros from PowerShell. Think of it as &lt;code&gt;docker&lt;/code&gt; but for Linux distros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking is NAT'd by default&lt;/strong&gt;: WSL2 gets its own IP that changes on reboot. Windows 11 23H2+ adds a mirrored networking mode that eliminates this.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows 11 (Build 22000 or later — verify with &lt;code&gt;winver&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;PowerShell running as Administrator&lt;/li&gt;
&lt;li&gt;Virtualization enabled in BIOS (check Task Manager → Performance → CPU → "Virtualization: Enabled")&lt;/li&gt;
&lt;li&gt;4 GB free disk space minimum; 10 GB recommended for a comfortable base install&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install WSL2 and Debian
&lt;/h3&gt;

&lt;p&gt;Open PowerShell as Administrator (right-click Start → &lt;strong&gt;Terminal (Admin)&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast path — works on most Windows 11 machines:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables the WSL feature, sets version 2 as default, and installs Debian. Once the install finishes, a Debian terminal opens automatically and prompts you to create a user account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enter new UNIX username: yourname
New password:
Retype new password:
passwd: password updated successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The password won't display as you type — that's normal. Once done, close the Debian window and reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Restart-Computer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The Microsoft Store's Debian package ships a patched &lt;code&gt;/init&lt;/code&gt; that silently breaks &lt;code&gt;systemd&lt;/code&gt; — services you enable will appear to succeed but never start. Use &lt;code&gt;wsl --install&lt;/code&gt; above, not the Store app, if you need reliable service management.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Manual path — use this if &lt;code&gt;wsl --install&lt;/code&gt; errors out:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;dism.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/online&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enable-feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/featurename:Microsoft-Windows-Subsystem-Linux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/norestart&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dism.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/online&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enable-feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/featurename:VirtualMachinePlatform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/norestart&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--set-default-version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reboot to activate the hypervisor, then after restart run &lt;code&gt;wsl --install -d Debian&lt;/code&gt; again — the username/password prompt will appear automatically when install completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Launch Debian After Reboot
&lt;/h3&gt;

&lt;p&gt;After rebooting, Debian won't open automatically. Launch it from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should land at your user prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yourname@MACHINE-NAME:~&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt; Launch it from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Default Version: 2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Kernel version: 5.15.x or higher&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# NAME      STATE     VERSION&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Debian    Running   2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If VERSION shows &lt;code&gt;1&lt;/code&gt;, force the upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--set-version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Launch Debian — Four Ways
&lt;/h3&gt;

&lt;p&gt;After first-launch setup, Debian won't automatically open on future reboots. Pick whichever method fits your workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From PowerShell or CMD (fastest):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just &lt;code&gt;wsl&lt;/code&gt; if Debian is your default distro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the Start menu:&lt;/strong&gt;&lt;br&gt;
Search for &lt;strong&gt;Debian&lt;/strong&gt; — it appears as an app. Pin it to your taskbar for one-click access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Windows Terminal:&lt;/strong&gt;&lt;br&gt;
Click the &lt;strong&gt;&lt;code&gt;+&lt;/code&gt;&lt;/strong&gt; dropdown tab arrow → select &lt;strong&gt;Debian&lt;/strong&gt;. Each click opens a new tab in your existing terminal window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From any File Explorer folder:&lt;/strong&gt;&lt;br&gt;
Right-click inside a folder → &lt;strong&gt;Open Linux shell here&lt;/strong&gt; (Windows 11 with Terminal installed). Opens a Debian shell with that folder already set as the working directory — useful when you want to run Linux tools on Windows files.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you get dropped into a &lt;code&gt;root&lt;/code&gt; shell instead of your user account, check that &lt;code&gt;[user] default=yourname&lt;/code&gt; is set in &lt;code&gt;/etc/wsl.conf&lt;/code&gt; (Step 4 covers this).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  4. Enable systemd
&lt;/h3&gt;

&lt;p&gt;Inside your Debian terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/wsl.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[boot]
systemd=true

[user]
default=your_unix_username
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart WSL from PowerShell, then re-enter the distro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--no-pager&lt;/span&gt; status
&lt;span class="c"&gt;# State: running&lt;/span&gt;
&lt;span class="c"&gt;# (NOT "System is degraded" — if degraded, run: systemctl --failed)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Update and Install Base Packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl wget git build-essential &lt;span class="se"&gt;\&lt;/span&gt;
  ca-certificates gnupg lsb-release &lt;span class="se"&gt;\&lt;/span&gt;
  htop tmux jq unzip

&lt;span class="nb"&gt;sudo &lt;/span&gt;timedatectl set-timezone America/New_York   &lt;span class="c"&gt;# adjust to your zone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timedatectl
&lt;span class="c"&gt;# Local time: ...&lt;/span&gt;
&lt;span class="c"&gt;# System clock synchronized: yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Install Windows Terminal
&lt;/h3&gt;

&lt;p&gt;WSL2 works in the default console but Windows Terminal is significantly better — tabs, split panes, per-distro profiles, and proper font rendering for tools like &lt;code&gt;tmux&lt;/code&gt; and &lt;code&gt;htop&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.WindowsTerminal&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install from the Microsoft Store. Once installed, open it and your Debian distro will already appear as a profile in the &lt;code&gt;+&lt;/code&gt; dropdown — no configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open Windows Terminal → click the &lt;code&gt;+&lt;/code&gt; dropdown → confirm &lt;strong&gt;Debian&lt;/strong&gt; appears as a profile. Select it — you should land at your Debian user prompt, not a root shell.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Export a Backup Snapshot Before Installing Anything Else
&lt;/h3&gt;

&lt;p&gt;Before you install project-specific tooling, export a clean snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In PowerShell — create the backup directory first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\wsl-backups&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\wsl-backups\debian-clean.tar&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To restore from backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--unregister&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\WSL\Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\wsl-backups\debian-clean.tar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;whoami&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Should print your username, not root&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accessing Files Between Windows and Linux
&lt;/h2&gt;

&lt;p&gt;This trips up almost every first-timer. WSL2 runs in its own filesystem, but both sides can reach each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From inside Debian — access Windows files:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/c/Users/YourWindowsUsername/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Windows drives are mounted under &lt;code&gt;/mnt/&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; I/O across this boundary is 10–30× slower than native Linux I/O. Never run &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;git clone&lt;/code&gt;, or heavy build operations on files under &lt;code&gt;/mnt/c/&lt;/code&gt; — always keep project files in &lt;code&gt;~/&lt;/code&gt; inside the distro.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;From Windows Explorer — browse your Linux filesystem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Type this path directly in the Explorer address bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\\wsl$\Debian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens your Debian home directory in Windows Explorer. You can drag, copy, and edit files here as if they were Windows files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shortcut — open current Linux folder in Explorer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;explorer.exe &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this from any directory inside WSL2 and it opens that exact folder in Windows Explorer. Useful for accessing build artifacts from your IDE.&lt;/p&gt;

&lt;h2&gt;
  
  
  WSL Command Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --install -d Debian&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Install a specific distro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --list --online&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show all available distros&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl -l -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List installed distros with version and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop all running WSL instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update the WSL kernel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl -d Debian&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Launch a specific distro by name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --export Debian file.tar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Export distro to a backup file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --import Debian path file.tar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restore distro from backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --unregister Debian&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove a distro (destructive — backup first)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --set-version Debian 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Convert an existing distro to WSL2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Advanced Configuration
&lt;/h2&gt;

&lt;p&gt;These settings are optional — apply them once your base Debian environment is working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capping WSL2 Memory Usage
&lt;/h3&gt;

&lt;p&gt;The Hyper-V VM holds memory it has used even after processes exit. Without a cap it can consume several GB that Windows never reclaims. Create &lt;code&gt;C:\Users\YourName\.wslconfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[wsl2]&lt;/span&gt;
&lt;span class="py"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4GB&lt;/span&gt;
&lt;span class="py"&gt;processors&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply with &lt;code&gt;wsl --shutdown&lt;/code&gt;. Verify inside Debian: &lt;code&gt;free -h&lt;/code&gt; should show the capped total.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mirrored Networking (Windows 11 23H2+)
&lt;/h3&gt;

&lt;p&gt;By default, WSL2 uses NAT and gets a dynamic IP that changes on reboot. Mirrored mode gives WSL2 the same IP as your Windows host — services are reachable from your LAN without port-forwarding rules.&lt;/p&gt;

&lt;p&gt;Add to &lt;code&gt;C:\Users\YourName\.wslconfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[wsl2]&lt;/span&gt;
&lt;span class="py"&gt;networkingMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mirrored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply with &lt;code&gt;wsl --shutdown&lt;/code&gt;. Verify inside Debian: &lt;code&gt;ip addr&lt;/code&gt; should show your Windows LAN IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging &amp;amp; Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Error: 0x80370102&lt;/code&gt; — virtualization not enabled
&lt;/h3&gt;

&lt;p&gt;This is the most common error on first install. Reboot into BIOS/UEFI and enable &lt;strong&gt;Intel VT-x&lt;/strong&gt; (Intel CPUs) or &lt;strong&gt;AMD-V&lt;/strong&gt; (AMD CPUs). It's usually under Advanced → CPU Configuration. Some corporate laptops have this locked by IT policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distro stuck at "Installing" with 0% progress after reboot
&lt;/h3&gt;

&lt;p&gt;Two common causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Machine Platform not enabled&lt;/strong&gt; — rerun Step 1 and reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Store pending update&lt;/strong&gt; — open the Microsoft Store, check for pending updates, let them finish, then try again. The Debian app itself sometimes needs to complete a Store update before first launch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inspect the event log for the underlying error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LogName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft-Windows-Hyper-V-Guest-Drivers/Admin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MaxEvents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  systemctl reports "System is degraded"
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;systemctl --failed&lt;/code&gt; to see which units failed. Three common WSL2-specific culprits that are safe to mask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl mask multipathd.service      &lt;span class="c"&gt;# storage multipath — not applicable in WSL2&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl mask systemd-remount-fs.service  &lt;span class="c"&gt;# WSL2 filesystem quirk&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl mask apparmor.service        &lt;span class="c"&gt;# WSL2 kernel has no AppArmor modules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;wsl --install&lt;/code&gt; says WSL is already installed but nothing works
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;wsl --update&lt;/code&gt; to make sure the kernel is current, then &lt;code&gt;wsl --shutdown&lt;/code&gt; and try again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the distro still won't launch, check its status: &lt;code&gt;wsl -l -v&lt;/code&gt;. If STATE shows "Stopped" and it won't start, export it for backup, unregister, and re-import from your backup snapshot (Step 7, Export a Backup Snapshot).&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I run WSL2 on Windows 10?
&lt;/h3&gt;

&lt;p&gt;Yes — WSL2 is available on Windows 10 version 1903 (Build 18362) and later. However, native &lt;code&gt;systemd&lt;/code&gt; support requires Windows 11 22H2 or Windows 10 Build 22000+. On older Windows 10 builds you need the &lt;code&gt;genie&lt;/code&gt; or &lt;code&gt;distrod&lt;/code&gt; workaround to get systemd running. Everything else in this guide works as written.&lt;/p&gt;

&lt;h3&gt;
  
  
  My WSL2 distro is slow when accessing files under &lt;code&gt;/mnt/c/&lt;/code&gt;. Is this normal?
&lt;/h3&gt;

&lt;p&gt;That slowness is real and it doesn't go away. Cross-filesystem I/O crosses a virtual filesystem boundary — Linux talking to a Windows-hosted NTFS volume through a translation layer. Always keep active project files inside the Linux distro filesystem (&lt;code&gt;~/projects/&lt;/code&gt;), not under &lt;code&gt;/mnt/c/Users/...&lt;/code&gt;. The gap can be 10–30× on large file operations like &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;git clone&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I access my WSL2 Debian from another machine on my LAN?
&lt;/h3&gt;

&lt;p&gt;By default, WSL2 uses NAT and is not directly accessible from the LAN. Three options: (1) Windows port forwarding — &lt;code&gt;netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=8080 connectaddress=$(wsl hostname -I)&lt;/code&gt;. (2) Switch to mirrored networking in &lt;code&gt;.wslconfig&lt;/code&gt; under &lt;code&gt;[wsl2]&lt;/code&gt; — available on Windows 11 23H2+ and gives WSL2 the same IP as your Windows host. (3) Expose services via a reverse proxy like Caddy running inside the distro, bound to &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>wsl2</category>
      <category>debian</category>
      <category>windows</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
