{"id":4739,"date":"2026-03-19T21:31:37","date_gmt":"2026-03-19T16:01:37","guid":{"rendered":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/"},"modified":"2026-03-19T21:31:37","modified_gmt":"2026-03-19T16:01:37","slug":"docker-best-practices-build-faster-and-more-secure-images","status":"publish","type":"post","link":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/","title":{"rendered":"Docker Best Practices: Build Faster and More Secure Images"},"content":{"rendered":"<p>The smell of burnt coffee is the only thing keeping my eyes open. It\u2019s 04:15 AM. I\u2019ve been staring at a Grafana dashboard that looks like a heart monitor for a patient in active cardiac arrest for the last three days. My PagerDuty alert didn&#8217;t just beep; it screamed. It screamed because some &#8220;Full Stack Rockstar&#8221; decided that pinning versions was for cowards and that resource limits were &#8220;suggestions.&#8221;<\/p>\n<p>I\u2019m tired. I\u2019m cynical. I\u2019ve spent seventy-two hours cleaning up a mess that could have been avoided if anyone in the engineering department had read a single page of documentation written after 2014. We\u2019re running Docker Engine v24.0.7 on Debian Bullseye nodes, and yet, I\u2019m seeing patterns that belong in a hobbyist\u2019s &#8220;Hello World&#8221; project.<\/p>\n<p>Here is the post-mortem. Read it. Internalize it. Or get out of my cluster.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_80 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<label for=\"ez-toc-cssicon-toggle-item-69d8546c81272\" class=\"ez-toc-cssicon-toggle-label\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/label><input type=\"checkbox\"  id=\"ez-toc-cssicon-toggle-item-69d8546c81272\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#Timeline_of_the_Failure\" >Timeline of the Failure<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#1_The_Incident_03_00_AM_and_the_OOM_Killer_is_Hungry\" >1. The Incident: 03:00 AM and the OOM Killer is Hungry<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#2_The_Root_Cause_4GB_Images_and_the_%E2%80%9CIt_Works_on_My_Machine%E2%80%9D_Fallacy\" >2. The Root Cause: 4GB Images and the &#8220;It Works on My Machine&#8221; Fallacy<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#3_Layer_Optimization_Stop_Rebuilding_the_World_on_Every_Git_Commit\" >3. Layer Optimization: Stop Rebuilding the World on Every Git Commit<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#The_%E2%80%9CBroken%E2%80%9D_Way_The_Junior_Dev_Special\" >The &#8220;Broken&#8221; Way (The Junior Dev Special)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#The_%E2%80%9CSRE_Way%E2%80%9D_Hardened_Multi-stage\" >The &#8220;SRE Way&#8221; (Hardened Multi-stage)<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#4_The_Security_Gap_Why_Youre_Running_as_Root_and_Why_Im_Revoking_Your_SSH_Access\" >4. The Security Gap: Why You\u2019re Running as Root and Why I\u2019m Revoking Your SSH Access<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#5_Signal_Handling_Why_Your_App_Wont_Shut_Down_Gracefully_PID_1_Blues\" >5. Signal Handling: Why Your App Won&#8217;t Shut Down Gracefully (PID 1 Blues)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#6_The_%E2%80%9Cdocker_best%E2%80%9D_Manifesto_A_Checklist_for_People_Who_Dont_Want_to_Get_Fired\" >6. The &#8220;docker best&#8221; Manifesto: A Checklist for People Who Don&#8217;t Want to Get Fired<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#The_Checklist\" >The Checklist<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#Deep_Dive_The_Overlay2_Storage_Driver_and_Why_It_Hates_You\" >Deep Dive: The Overlay2 Storage Driver and Why It Hates You<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#The_Final_Word_on_Seccomp_and_Syscalls\" >The Final Word on Seccomp and Syscalls<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#Related_Articles\" >Related Articles<\/a><\/li><\/ul><\/nav><\/div>\n<h3><span class=\"ez-toc-section\" id=\"Timeline_of_the_Failure\"><\/span>Timeline of the Failure<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li><strong>2024-05-12 03:00:01 UTC:<\/strong> <code>node-04<\/code> reports 98% memory utilization. <code>systemd-oomd<\/code> begins monitoring.<\/li>\n<li><strong>2024-05-12 03:02:15 UTC:<\/strong> Kernel OOM Killer invokes <code>out_of_memory<\/code>. Victim: <code>java_app_container<\/code>.<\/li>\n<li><strong>2024-05-12 03:02:20 UTC:<\/strong> Scheduler attempts to restart container on <code>node-05<\/code>.<\/li>\n<li><strong>2024-05-12 03:05:40 UTC:<\/strong> <code>node-05<\/code> network interface saturates (10Gbps) attempting to pull <code>image:latest<\/code>.<\/li>\n<li><strong>2024-05-12 03:10:12 UTC:<\/strong> Cascading failure. Five nodes are stuck in <code>ImagePullBackOff<\/code> because the 4.2GB image layers are thrashing the <code>overlay2<\/code> storage driver.<\/li>\n<li><strong>2024-05-12 03:15:00 UTC:<\/strong> I am woken up by the sound of my career dying.<\/li>\n<\/ul>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"1_The_Incident_03_00_AM_and_the_OOM_Killer_is_Hungry\"><\/span>1. The Incident: 03:00 AM and the OOM Killer is Hungry<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The logs don&#8217;t lie. When I finally got a shell into <code>node-04<\/code>, <code>dmesg<\/code> was a graveyard. <\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">[10842.123456] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=\/,mems_allowed=0,oom_memcg=\/kubepods\/besteffort\/pod123,task_memcg=\/kubepods\/besteffort\/pod123,task=java,pid=12345,uid=0\n[10842.123500] Memory cgroup out of memory: Killed process 12345 (java) total-vm:8421024kB, anon-rss:4194304kB, file-rss:0kB, shmem-rss:0kB\n<\/code><\/pre>\n<p>You see that <code>uid=0<\/code>? We\u2019ll get to that. But look at the <code>anon-rss<\/code>. 4GB. The container had no limits set in the <code>docker-compose.yml<\/code> or the manifest. It just kept eating. In the world of Linux namespaces and cgroups, a container without a limit is a suicide pact. The kernel doesn&#8217;t care about your &#8220;critical business logic.&#8221; It sees a process hogging pages, and it executes it.<\/p>\n<p>The &#8220;fix&#8221; isn&#8217;t just adding a <code>mem_limit<\/code>. It\u2019s understanding how the JVM interacts with cgroups. If you don&#8217;t use <code>-XX:+UseContainerSupport<\/code>, the JVM looks at the host&#8217;s total memory, not the container&#8217;s limit. You\u2019re lying to your app, and the kernel is the debt collector.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"2_The_Root_Cause_4GB_Images_and_the_%E2%80%9CIt_Works_on_My_Machine%E2%80%9D_Fallacy\"><\/span>2. The Root Cause: 4GB Images and the &#8220;It Works on My Machine&#8221; Fallacy<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I pulled the image that caused the saturation. <code>docker inspect<\/code> revealed a horror show. <\/p>\n<pre class=\"codehilite\"><code class=\"language-json\">&quot;RootFS&quot;: {\n    &quot;Type&quot;: &quot;layers&quot;,\n    &quot;Layers&quot;: [\n        &quot;sha256:7297da16...&quot;, \n        &quot;sha256:8527027...&quot;,\n        &quot;sha256:...&quot; \/\/ 42 more layers of pure incompetence\n    ]\n}\n<\/code><\/pre>\n<p>The developer used <code>Ubuntu:latest<\/code> as a base. Then they ran <code>apt-get update<\/code> and <code>apt-get install<\/code> in six different <code>RUN<\/code> commands. They left the <code>apt<\/code> cache in the image. They included the entire build-time toolchain\u2014GCC, Python, Go, and probably a copy of the Oxford English Dictionary\u2014in the production runtime image.<\/p>\n<p>This is the &#8220;Layer Cake of Lies.&#8221; Every <code>RUN<\/code> command creates a new layer on the <code>overlay2<\/code> filesystem. If you delete a file in a subsequent layer, it\u2019s not gone; it\u2019s just hidden by a &#8220;whiteout&#8221; file. The bits are still there, taking up space, slowing down <code>docker pull<\/code>, and increasing the attack surface. <\/p>\n<p>When the cluster tried to recover, it had to move 4.2GB across the wire for every single pod. Our internal registry&#8217;s disk I\/O spiked so hard the metadata database started throwing 500s. We weren&#8217;t just down; we were dead-locked.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"3_Layer_Optimization_Stop_Rebuilding_the_World_on_Every_Git_Commit\"><\/span>3. Layer Optimization: Stop Rebuilding the World on Every Git Commit<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I looked at the <code>Dockerfile<\/code>. It was a masterpiece of inefficiency.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"The_%E2%80%9CBroken%E2%80%9D_Way_The_Junior_Dev_Special\"><\/span>The &#8220;Broken&#8221; Way (The Junior Dev Special)<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<pre class=\"codehilite\"><code class=\"language-dockerfile\">FROM ubuntu:latest\n# No version pinning. Good luck in six months.\nRUN apt-get update\nRUN apt-get install -y nodejs npm\nCOPY . \/app\n# This COPY is the kiss of death. \n# Any change to a README.md invalidates the cache for everything below.\nRUN npm install\nWORKDIR \/app\nCMD [&quot;node&quot;, &quot;server.js&quot;]\n<\/code><\/pre>\n<p>Every time a dev changed a single line of CSS, the <code>RUN npm install<\/code> would trigger. That\u2019s 500MB of <code>node_modules<\/code> being downloaded and compressed into a new layer. Every. Single. Time.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"The_%E2%80%9CSRE_Way%E2%80%9D_Hardened_Multi-stage\"><\/span>The &#8220;SRE Way&#8221; (Hardened Multi-stage)<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>To ensure long-term stability and keep the &#8220;docker best&#8221; practices alive, we use multi-stage builds and specific base images. We use <code>Alpine 3.19<\/code> or <code>Debian Bookworm-slim<\/code>. We pin versions. We respect the cache.<\/p>\n<pre class=\"codehilite\"><code class=\"language-dockerfile\"># Stage 1: Build\nFROM node:20.11.0-bookworm-slim AS builder\nWORKDIR \/app\n# Only copy files needed for dependency resolution\nCOPY package.json package-lock.json .\/\nRUN npm ci --only=production\n\n# Stage 2: Runtime\nFROM node:20.11.0-bookworm-slim\nRUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \\\n    dumb-init=1.2.5-2 \\\n    &amp;&amp; rm -rf \/var\/lib\/apt\/lists\/*\n\nWORKDIR \/app\n# Copy only the artifacts from the builder\nCOPY --from=builder \/app\/node_modules .\/node_modules\nCOPY . .\n\n# Security: Don't run as root\nUSER node\nENTRYPOINT [&quot;\/usr\/bin\/dumb-init&quot;, &quot;--&quot;]\nCMD [&quot;node&quot;, &quot;server.js&quot;]\n<\/code><\/pre>\n<p>By separating the build environment from the runtime environment, we dropped the image size from 4.2GB to 180MB. We used <code>npm ci<\/code> for deterministic builds. We cleaned up the <code>apt<\/code> lists in the same <code>RUN<\/code> command to prevent layer bloat. This isn&#8217;t just &#8220;optimization&#8221;; it&#8217;s survival.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"4_The_Security_Gap_Why_Youre_Running_as_Root_and_Why_Im_Revoking_Your_SSH_Access\"><\/span>4. The Security Gap: Why You\u2019re Running as Root and Why I\u2019m Revoking Your SSH Access<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>During the incident, I noticed something even more disturbing. One of the compromised containers had a shell history that wasn&#8217;t mine. Because the container was running as <code>root<\/code> (the default in Docker if you\u2019re lazy), a simple remote code execution (RCE) vulnerability in the web app gave the attacker a root shell inside the container.<\/p>\n<p>From there, they checked for the existence of <code>\/var\/run\/docker.sock<\/code>. And guess what? Some &#8220;DevOps Engineer&#8221; had mounted it so the container could &#8220;manage other containers.&#8221;<\/p>\n<p>Mounting the Docker socket is equivalent to giving the container <code>sudo<\/code> access to the host without a password. It\u2019s a container escape waiting to happen. An attacker can just run <code>docker run -v \/:\/host -it ubuntu:latest chroot \/host<\/code> and they own the entire node. They own the kernel. They own the data.<\/p>\n<p>We use <code>seccomp<\/code> profiles and <code>AppArmor<\/code>. We restrict syscalls. Does your app really need <code>mount()<\/code>, <code>ptrace()<\/code>, or <code>kexec_load()<\/code>? No? Then why are they available to your container? Docker Engine v24 ships with a default seccomp profile, but it\u2019s broad. A &#8220;docker best&#8221; approach involves profiling the app and dropping every capability except <code>NET_BIND_SERVICE<\/code>.<\/p>\n<p>And for the love of all that is holy, stop using <code>latest<\/code>. <code>latest<\/code> is not a version. It\u2019s a moving target. It\u2019s a rolling dice. When you pull <code>latest<\/code>, you have no idea what code is actually running. You can\u2019t roll back because <code>latest<\/code> today is different from <code>latest<\/code> yesterday. Use SHA256 hashes if you\u2019re serious, or at least specific SemVer tags.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"5_Signal_Handling_Why_Your_App_Wont_Shut_Down_Gracefully_PID_1_Blues\"><\/span>5. Signal Handling: Why Your App Won&#8217;t Shut Down Gracefully (PID 1 Blues)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>During the meltdown, I tried to restart the services. <code>docker stop<\/code> took 10 seconds for every container. Why? Because your app is running as PID 1 and it\u2019s deaf to signals.<\/p>\n<p>When you use the &#8220;shell form&#8221; of <code>CMD<\/code> or <code>ENTRYPOINT<\/code>:<br \/>\n<code>CMD node server.js<\/code><\/p>\n<p>Docker executes it as <code>\/bin\/sh -c \"node server.js\"<\/code>. The shell starts as PID 1. When Docker sends a <code>SIGTERM<\/code> to the container, it goes to the shell. The shell, being a stubborn piece of 1970s technology, does not forward that signal to the child process (node). The app keeps running, unaware that the reaper is coming. After 10 seconds, Docker loses patience and sends <code>SIGKILL<\/code>.<\/p>\n<p><code>SIGKILL<\/code> is the nuclear option. It doesn&#8217;t allow the app to close database connections, flush buffers, or finish processing a request. It just stops the CPU in its tracks. This leads to database corruption and &#8220;zombie&#8221; records.<\/p>\n<p>The fix is the &#8220;exec form&#8221;:<br \/>\n<code>CMD [\"node\", \"server.js\"]<\/code><\/p>\n<p>Or better yet, use <code>dumb-init<\/code> or <code>tini<\/code>. These are tiny init systems designed to run as PID 1, reap zombie processes, and correctly forward signals. If your app doesn&#8217;t shut down in under a second, you\u2019ve failed.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"6_The_%E2%80%9Cdocker_best%E2%80%9D_Manifesto_A_Checklist_for_People_Who_Dont_Want_to_Get_Fired\"><\/span>6. The &#8220;docker best&#8221; Manifesto: A Checklist for People Who Don&#8217;t Want to Get Fired<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I\u2019m going to go get another coffee. While I\u2019m gone, you\u2019re going to rewrite your <code>docker-compose.yml<\/code> and your <code>Dockerfile<\/code>s. If I see another 4GB image or a container running as root, I\u2019m revoking your production access and moving you to the documentation team.<\/p>\n<p>Here is the <code>docker-compose.yml<\/code> that I expect to see. It uses <code>version: '3.8'<\/code> syntax (or the newer Compose Specification) and defines strict resource constraints.<\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">services:\n  api-gateway:\n    image: our-registry.io\/api-gateway:v2.4.1@sha256:a1b2c3d4...\n    deploy:\n      resources:\n        limits:\n          cpus: '0.50'\n          memory: 512M\n        reservations:\n          cpus: '0.25'\n          memory: 256M\n    restart: on-failure:3\n    security_opt:\n      - no-new-privileges:true\n    read_only: true\n    tmpfs:\n      - \/tmp\n      - \/run\n    logging:\n      driver: &quot;json-file&quot;\n      options:\n        max-size: &quot;10m&quot;\n        max-file: &quot;3&quot;\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http:\/\/localhost:8080\/health&quot;]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 40s\n<\/code><\/pre>\n<h3><span class=\"ez-toc-section\" id=\"The_Checklist\"><\/span>The Checklist<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ol>\n<li><strong>No Root Users:<\/strong> Always define a <code>USER<\/code> in your Dockerfile. If you need to bind to a port below 1024, use <code>CAP_NET_BIND_SERVICE<\/code>, not root.<\/li>\n<li><strong>Explicit Resource Limits:<\/strong> If you don&#8217;t set a <code>memory_limit<\/code>, you are volunteering to be the first person I call at 3 AM.<\/li>\n<li><strong>Multi-Stage Builds:<\/strong> If your production image contains a compiler, you\u2019ve done it wrong.<\/li>\n<li><strong>Small Base Images:<\/strong> Use <code>Alpine<\/code> or <code>distroless<\/code>. If you use <code>Ubuntu<\/code>, use the <code>-slim<\/code> variant and clean up your mess.<\/li>\n<li><strong>Immutable Tags:<\/strong> <code>latest<\/code> is a crime. Pin your versions. Pin your SHA hashes.<\/li>\n<li><strong>Read-Only Filesystems:<\/strong> Use <code>read_only: true<\/code> in your compose file. If your app needs to write to a temp directory, use a <code>tmpfs<\/code> mount. This kills 90% of automated exploit payloads.<\/li>\n<li><strong>Signal Handling:<\/strong> Use <code>exec<\/code> form for <code>CMD<\/code>. Use an init process like <code>tini<\/code>.<\/li>\n<li><strong>The .dockerignore File:<\/strong> Stop sending your <code>.git<\/code> folder and <code>node_modules<\/code> to the Docker daemon. It slows down the build and leaks secrets.<\/li>\n<\/ol>\n<p>I\u2019ve spent a decade in the Linux terminal. I\u2019ve seen <code>ext4<\/code> filesystems dissolve like cotton candy in the rain. I\u2019ve seen <code>iptables<\/code> rules that would make a cryptographer weep. Docker is a tool, not a magic wand. If you treat it like a &#8220;black box&#8221; where you can just throw your garbage code and expect it to run forever, you are the problem.<\/p>\n<p>The cluster didn&#8217;t scream because of a bug in Docker. It screamed because of you. It screamed because it was bloated, insecure, and unmanaged. <\/p>\n<p>Go fix your images. I\u2019m going to sleep. If my pager goes off again because of an OOM event, I\u2019m not fixing the cluster\u2014I\u2019m fixing the hiring process.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Deep_Dive_The_Overlay2_Storage_Driver_and_Why_It_Hates_You\"><\/span>Deep Dive: The Overlay2 Storage Driver and Why It Hates You<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Since you&#8217;re still here, let&#8217;s talk about why your 4GB image actually breaks the disk. Docker uses the <code>overlay2<\/code> driver. It works by stacking &#8220;lower&#8221; directories and a single &#8220;upper&#8221; directory. When you write a file, it uses &#8220;copy-on-write&#8221; (CoW). <\/p>\n<p>If you have a 1GB log file in a lower layer and you run <code>chmod 777<\/code> on it in your Dockerfile, <code>overlay2<\/code> has to copy that entire 1GB file to the new layer just to change the permission bits. You now have 2GB of disk usage for a 1GB file. This is why we chain commands:<\/p>\n<pre class=\"codehilite\"><code class=\"language-dockerfile\"># WRONG\nRUN wget http:\/\/huge-file.tar.gz\nRUN tar -xvf huge-file.tar.gz\nRUN rm huge-file.tar.gz\n\n# RIGHT\nRUN wget http:\/\/huge-file.tar.gz &amp;&amp; \\\n    tar -xvf huge-file.tar.gz &amp;&amp; \\\n    rm huge-file.tar.gz\n<\/code><\/pre>\n<p>In the &#8220;RIGHT&#8221; example, the file is downloaded, extracted, and deleted in a <em>single layer<\/em>. The <code>overlay2<\/code> driver never has to commit the <code>.tar.gz<\/code> to disk permanently. This is &#8220;docker best&#8221; practice 101, and yet, I see people failing it every single day.<\/p>\n<p>And don&#8217;t get me started on <code>storage-opts<\/code>. We&#8217;re running <code>overlay2.override_kernel_check=true<\/code> because we know our kernel (6.1 LTS) can handle it. We\u2019ve tuned the <code>xfs<\/code> backing store for the <code>\/var\/lib\/docker<\/code> partition with <code>pquota<\/code> to prevent a single container from filling the entire host&#8217;s disk with logs. Did you know you could do that? No, you were too busy adding another <code>RUN<\/code> command to your Dockerfile.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"The_Final_Word_on_Seccomp_and_Syscalls\"><\/span>The Final Word on Seccomp and Syscalls<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>If you really want to impress me, stop looking at Docker as a way to &#8220;package apps&#8221; and start looking at it as a way to &#8220;sandbox processes.&#8221; <\/p>\n<p>A container is just a process with a fancy hat. It still talks to the same kernel. Every time your app makes a syscall\u2014<code>open()<\/code>, <code>read()<\/code>, <code>write()<\/code>, <code>socket()<\/code>\u2014it\u2019s an opportunity for something to go wrong. By using a custom <code>seccomp<\/code> profile, you can restrict the process so it can <em>only<\/em> do what it\u2019s supposed to do. <\/p>\n<p>If your Node.js app starts trying to call <code>execve()<\/code> to run a shell script it just downloaded into <code>\/tmp<\/code>, a good <code>seccomp<\/code> profile will kill the process instantly. That\u2019s the difference between a &#8220;security incident&#8221; and a &#8220;blocked syscall&#8221; log entry.<\/p>\n<p>I\u2019m done. The sun is coming up. The cluster is stable, for now. Don&#8217;t make me come back here. Fix your habits. Respect the cgroups. And never, ever use <code>latest<\/code> again.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Related_Articles\"><\/span>Related Articles<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Explore more insights and best practices:<\/p>\n<ul>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/install-node-js-in-ubuntu\/\">Install Node Js In Ubuntu<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/master-aws-best-practices-optimize-your-cloud-performance\/\">Master Aws Best Practices Optimize Your Cloud Performance<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/cybersecurity-near-me-guide\/\">Cybersecurity Near Me Guide<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>The smell of burnt coffee is the only thing keeping my eyes open. It\u2019s 04:15 AM. I\u2019ve been staring at a Grafana dashboard that looks like a heart monitor for a patient in active cardiac arrest for the last three days. My PagerDuty alert didn&#8217;t just beep; it screamed. It screamed because some &#8220;Full Stack &#8230; <a title=\"Docker Best Practices: Build Faster and More Secure Images\" class=\"read-more\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\" aria-label=\"Read more  on Docker Best Practices: Build Faster and More Secure Images\">Read more<\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4739","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Docker Best Practices: Build Faster and More Secure Images - ITSupportWale<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Docker Best Practices: Build Faster and More Secure Images - ITSupportWale\" \/>\n<meta property=\"og:description\" content=\"The smell of burnt coffee is the only thing keeping my eyes open. It\u2019s 04:15 AM. I\u2019ve been staring at a Grafana dashboard that looks like a heart monitor for a patient in active cardiac arrest for the last three days. My PagerDuty alert didn&#8217;t just beep; it screamed. It screamed because some &#8220;Full Stack ... Read more\" \/>\n<meta property=\"og:url\" content=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\" \/>\n<meta property=\"og:site_name\" content=\"ITSupportWale\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Itsupportwale-298547177495978\" \/>\n<meta property=\"article:published_time\" content=\"2026-03-19T16:01:37+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2021\/05\/android-chrome-512x512-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"512\" \/>\n\t<meta property=\"og:image:height\" content=\"512\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Techie\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Techie\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\"},\"author\":{\"name\":\"Techie\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\"},\"headline\":\"Docker Best Practices: Build Faster and More Secure Images\",\"datePublished\":\"2026-03-19T16:01:37+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\"},\"wordCount\":1831,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\",\"name\":\"Docker Best Practices: Build Faster and More Secure Images - ITSupportWale\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\"},\"datePublished\":\"2026-03-19T16:01:37+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/itsupportwale.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Docker Best Practices: Build Faster and More Secure Images\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\",\"url\":\"https:\/\/itsupportwale.com\/blog\/\",\"name\":\"ITSupportWale\",\"description\":\"Tips, Tricks, Fixed-Errors, Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/itsupportwale.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\",\"name\":\"itsupportwale\",\"url\":\"https:\/\/itsupportwale.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png\",\"contentUrl\":\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png\",\"width\":1119,\"height\":144,\"caption\":\"itsupportwale\"},\"image\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/Itsupportwale-298547177495978\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\",\"name\":\"Techie\",\"sameAs\":[\"https:\/\/itsupportwale.com\",\"iswblogadmin\"],\"url\":\"https:\/\/itsupportwale.com\/blog\/author\/iswblogadmin\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Docker Best Practices: Build Faster and More Secure Images - ITSupportWale","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/","og_locale":"en_US","og_type":"article","og_title":"Docker Best Practices: Build Faster and More Secure Images - ITSupportWale","og_description":"The smell of burnt coffee is the only thing keeping my eyes open. It\u2019s 04:15 AM. I\u2019ve been staring at a Grafana dashboard that looks like a heart monitor for a patient in active cardiac arrest for the last three days. My PagerDuty alert didn&#8217;t just beep; it screamed. It screamed because some &#8220;Full Stack ... Read more","og_url":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/","og_site_name":"ITSupportWale","article_publisher":"https:\/\/www.facebook.com\/Itsupportwale-298547177495978","article_published_time":"2026-03-19T16:01:37+00:00","og_image":[{"width":512,"height":512,"url":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2021\/05\/android-chrome-512x512-1.png","type":"image\/png"}],"author":"Techie","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Techie","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#article","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/"},"author":{"name":"Techie","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d"},"headline":"Docker Best Practices: Build Faster and More Secure Images","datePublished":"2026-03-19T16:01:37+00:00","mainEntityOfPage":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/"},"wordCount":1831,"commentCount":0,"publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/","url":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/","name":"Docker Best Practices: Build Faster and More Secure Images - ITSupportWale","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/#website"},"datePublished":"2026-03-19T16:01:37+00:00","breadcrumb":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-faster-and-more-secure-images\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/itsupportwale.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Docker Best Practices: Build Faster and More Secure Images"}]},{"@type":"WebSite","@id":"https:\/\/itsupportwale.com\/blog\/#website","url":"https:\/\/itsupportwale.com\/blog\/","name":"ITSupportWale","description":"Tips, Tricks, Fixed-Errors, Tutorials &amp; Guides","publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/itsupportwale.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/itsupportwale.com\/blog\/#organization","name":"itsupportwale","url":"https:\/\/itsupportwale.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png","contentUrl":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png","width":1119,"height":144,"caption":"itsupportwale"},"image":{"@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/Itsupportwale-298547177495978"]},{"@type":"Person","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d","name":"Techie","sameAs":["https:\/\/itsupportwale.com","iswblogadmin"],"url":"https:\/\/itsupportwale.com\/blog\/author\/iswblogadmin\/"}]}},"_links":{"self":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4739","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/comments?post=4739"}],"version-history":[{"count":0,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4739\/revisions"}],"wp:attachment":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/media?parent=4739"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/categories?post=4739"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/tags?post=4739"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}