{"id":4895,"date":"2026-10-02T01:47:18","date_gmt":"2026-10-01T20:17:18","guid":{"rendered":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/"},"modified":"2026-10-02T01:47:18","modified_gmt":"2026-10-01T20:17:18","slug":"docker-tutorial-build-ship-and-run-apps-anywhere","status":"publish","type":"post","link":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/","title":{"rendered":"Docker Tutorial: Build, Ship, and Run Apps Anywhere"},"content":{"rendered":"<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-6ac36b84f0403\" 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-6ac36b84f0403\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#Docker_is_Not_a_Virtual_Machine_and_Your_Images_are_Too_Fat\" >Docker is Not a Virtual Machine, and Your Images are Too Fat<\/a><\/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-tutorial-build-ship-and-run-apps-anywhere\/#The_Documentation_Lies_to_You\" >The Documentation Lies to You<\/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-tutorial-build-ship-and-run-apps-anywhere\/#The_Meat_Layers_Cache_and_the_Union_File_System\" >The Meat: Layers, Cache, and the Union File System<\/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-tutorial-build-ship-and-run-apps-anywhere\/#The_Alpine_Trap_glibc_vs_musl\" >The Alpine Trap: glibc vs. musl<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#The_Networking_Nightmare\" >The Networking Nightmare<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#PID_1_and_the_Zombie_Apocalypse\" >PID 1 and the Zombie Apocalypse<\/a><\/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-tutorial-build-ship-and-run-apps-anywhere\/#Storage_Volumes_vs_Binds\" >Storage: Volumes vs. Binds<\/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-tutorial-build-ship-and-run-apps-anywhere\/#The_%E2%80%9CReal_World%E2%80%9D_Gotcha_The_OOM_Killer\" >The &#8220;Real World&#8221; Gotcha: The OOM Killer<\/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-tutorial-build-ship-and-run-apps-anywhere\/#Deep_Dive_The_Container_Runtime_Interface_CRI\" >Deep Dive: The Container Runtime Interface (CRI)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#Troubleshooting_Like_a_Pro\" >Troubleshooting Like a Pro<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#The_Registry_Bottleneck\" >The Registry Bottleneck<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#The_Wrap-up\" >The Wrap-up<\/a><\/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-tutorial-build-ship-and-run-apps-anywhere\/#Related_Articles\" >Related Articles<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"Docker_is_Not_a_Virtual_Machine_and_Your_Images_are_Too_Fat\"><\/span>Docker is Not a Virtual Machine, and Your Images are Too Fat<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>It was 3:14 AM on a Tuesday in 2017. I was the &#8220;on-call hero&#8221; for a fintech startup that shall remain nameless. We had a monolithic Python API that we\u2019d recently &#8220;containerized&#8221; to be modern. I pushed a change to the <code>master<\/code> branch, the CI\/CD pipeline hummed along, and the deployment triggered. Ten minutes later, the PagerDuty alert started screaming. Not just one service. Everything. The Kubelet on our primary nodes was reporting <code>DiskPressure<\/code> and started evicting pods like a bouncer at a dive bar. I\u2019d forgotten to add a <code>.dockerignore<\/code> file. My local <code>venv<\/code>, 4GB of temporary data science models, and a massive <code>.git<\/code> folder had been sucked into the build context, pushed to our private registry, and pulled onto every node simultaneously. The nodes ran out of disk space, the container runtime choked, and the entire cluster entered a death spiral.<\/p>\n<p>I spent the next four hours manually cleaning up <code>\/var\/lib\/docker\/overlay2<\/code> on six different instances while the CTO watched the Slack channel in silence. That\u2019s the reality of Docker. It isn&#8217;t a &#8220;seamless&#8221; abstraction layer. It\u2019s a leaky bucket of kernel namespaces, cgroups, and filesystem layers that will bite you the moment you stop respecting the underlying Linux primitives. If you treat it like a &#8220;lightweight VM,&#8221; you\u2019ve already lost. Docker is a process wrapper with an identity crisis. Let\u2019s stop pretending the &#8220;Hello World&#8221; tutorial is enough to run production systems.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Documentation_Lies_to_You\"><\/span>The Documentation Lies to You<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Most Docker documentation is written for developers who want to run a database on their laptop. It\u2019s not written for SREs who have to manage 500 nodes in <code>us-east-1<\/code>. The docs tell you that <code>FROM python:3.9<\/code> is a great starting point. It\u2019s not. That image is nearly 900MB because it includes every build tool, header file, and obscure library you\u2019ll never use. In production, every megabyte is a liability. It\u2019s more time spent in <code>docker pull<\/code>, more money spent on NAT Gateway egress, and a larger attack surface for the next CVE-2024-whatever.<\/p>\n<p>The industry is obsessed with the &#8220;Dockerize everything&#8221; hype, but we rarely talk about the cost of the abstraction. We\u2019ve traded &#8220;it works on my machine&#8221; for &#8220;it works in the container but the kernel is OOM-killing the process because I didn&#8217;t set cgroup limits correctly.&#8221; Docker is a tool for packaging, not a substitute for understanding how Linux manages resources. If you don&#8217;t know what <code>set -e<\/code> does in a shell script, you shouldn&#8217;t be writing Dockerfiles.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Meat_Layers_Cache_and_the_Union_File_System\"><\/span>The Meat: Layers, Cache, and the Union File System<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Docker images are just a stack of tarballs. That\u2019s it. When you see <code>Step 4\/10 : RUN apt-get update<\/code>, Docker is creating a new layer. If you change a line at the top of your Dockerfile, every layer below it is invalidated. This is where most people fail at &#8220;Docker 101.&#8221;<\/p>\n<pre><code># BAD DOCKERFILE\nFROM node:18\nCOPY . \/app\nWORKDIR \/app\nRUN npm install\nCMD [\"node\", \"index.js\"]\n<\/code><\/pre>\n<p>In the example above, every time you change a single character in a comment in <code>index.js<\/code>, Docker re-runs <code>npm install<\/code>. You\u2019re wasting five minutes of CI time and downloading half the internet for no reason. You have to exploit the layer cache. You copy the dependency manifest first, install, and <i>then<\/i> copy the source code.<\/p>\n<pre><code># BETTER DOCKERFILE\nFROM node:18-slim\nWORKDIR \/app\nCOPY package.json package-lock.json .\/\nRUN npm ci --production\nCOPY . .\nUSER node\nCMD [\"node\", \"index.js\"]\n<\/code><\/pre>\n<p>But even this is amateur hour. If you\u2019re a Senior SRE, you should be using multi-stage builds. There is zero reason for your production image to contain a compiler, a git client, or your <code>ssh<\/code> keys. You build the binary in one stage and copy it to a &#8220;distroless&#8221; or minimal base in the second.<\/p>\n<blockquote><p>\n    Pro-tip: Use <code>--mount=type=cache<\/code> with BuildKit to persist your package manager&#8217;s cache between builds. It\u2019s the difference between a 2-minute build and a 10-second build.\n<\/p><\/blockquote>\n<pre><code># THE ADULT WAY (Multi-stage + BuildKit Cache)\n# syntax=docker\/dockerfile:1.4\nFROM golang:1.21-alpine AS builder\nWORKDIR \/src\nRUN --mount=type=cache,target=\/go\/pkg\/mod \\\n    --mount=type=bind,source=go.sum,target=go.sum \\\n    --mount=type=bind,source=go.mod,target=go.mod \\\n    go mod download\nCOPY . .\nRUN go build -o \/bin\/api .\/cmd\/api\n\nFROM alpine:3.18\nRUN apk add --no-cache ca-certificates tzdata\nCOPY --from=builder \/bin\/api \/bin\/api\nUSER 1000\nENTRYPOINT [\"\/bin\/api\"]\n<\/code><\/pre>\n<p>Notice the <code>USER 1000<\/code>. If I see <code>root<\/code> running your app in production, I\u2019m revoking your SSH access. Containers don&#8217;t provide a security boundary by default. If your process is root inside the container and someone escapes via a kernel exploit, they\u2019re root on the host. It\u2019s that simple.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Alpine_Trap_glibc_vs_musl\"><\/span>The Alpine Trap: glibc vs. musl<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Everyone loves Alpine because it\u2019s 5MB. It\u2019s the &#8220;SRE&#8217;s darling.&#8221; But Alpine uses <code>musl<\/code> instead of <code>glibc<\/code>. If you\u2019re running Go or Rust, you probably won&#8217;t notice until you try to use a C-binding that expects <code>glibc<\/code>. If you\u2019re running Python, you\u2019re in for a world of hurt. Most Python wheels (pre-compiled binaries) are built for <code>manylinux<\/code> (glibc). When you <code>pip install pandas<\/code> on Alpine, it can&#8217;t find a compatible wheel, so it starts compiling from source. Your 30-second build just became a 20-minute build, and your &#8220;small&#8221; image is now bloated with <code>gcc<\/code> and <code>make<\/code> just to get the thing to install.<\/p>\n<p>I use <code>debian-slim<\/code>. It\u2019s 30MB larger, but it uses <code>glibc<\/code>. It\u2019s predictable. It doesn&#8217;t have weird DNS resolution bugs because <code>musl<\/code> handles <code>\/etc\/resolv.conf<\/code> differently than every other Linux distro. Stop chasing the 5MB dragon and start chasing the &#8220;I don&#8217;t want to debug DNS at 2 AM&#8221; dragon.<\/p>\n<ul>\n<li><b>Alpine:<\/b> Great for static binaries (Go, Rust) or simple utilities (curl, jq).<\/li>\n<li><b>Debian-Slim:<\/b> The gold standard for Python, Node, and Ruby.<\/li>\n<li><b>Distroless:<\/b> The final boss of security. No shell, no package manager, just your binary.<\/li>\n<li><b>Ubuntu:<\/b> Only if you really need a specific PPA or outdated library.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"The_Networking_Nightmare\"><\/span>The Networking Nightmare<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Docker networking is a mess of <code>iptables<\/code> rules that will make your head spin. By default, Docker uses the <code>bridge<\/code> driver. It creates a virtual bridge (<code>docker0<\/code>), assigns an IP range, and uses NAT to let containers talk to the outside world. This is fine for your local dev environment. It\u2019s a disaster for high-performance networking.<\/p>\n<p>Every packet going through that bridge has to be processed by the NAT engine. If you\u2019re running a high-throughput database or a proxy like Nginx, you\u2019ll see a measurable latency hit. This is why we use <code>--network=host<\/code> for performance-critical services, though it comes with the massive downside of sharing the host&#8217;s network namespace (no port isolation).<\/p>\n<p>And don&#8217;t get me started on MTU (Maximum Transmission Unit). I once spent three days debugging why a container could <code>curl<\/code> a small JSON payload but timed out on a 10MB file. The host was on an AWS VPC with an MTU of 9001 (Jumbo Frames), but the Docker bridge was defaulted to 1500. The packets were being dropped silently. Note to self: Always check <code>ip addr show docker0<\/code> when things get weird.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"PID_1_and_the_Zombie_Apocalypse\"><\/span>PID 1 and the Zombie Apocalypse<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In Linux, PID 1 is special. It\u2019s the <code>init<\/code> process. It\u2019s responsible for reaping &#8220;zombie&#8221; processes (processes that have finished but haven&#8217;t been acknowledged by their parent). Most applications (Node, Python, Java) are not designed to be PID 1. They don&#8217;t handle signals like <code>SIGTERM<\/code> or <code>SIGINT<\/code> correctly, and they definitely don&#8217;t reap orphans.<\/p>\n<p>If you run <code>docker run my-app<\/code>, and your app spawns subprocesses, those subprocesses will eventually become zombies when they die. They\u2019ll stay in the process table until the container is restarted. Even worse, when you run <code>docker stop<\/code>, Docker sends <code>SIGTERM<\/code> to PID 1. If your app doesn&#8217;t explicitly catch that signal, Docker waits 10 seconds and then <code>SIGKILL<\/code>s it. Your app didn&#8217;t shut down gracefully. It didn&#8217;t close database connections. It didn&#8217;t finish the last request. It just died.<\/p>\n<p>The fix is <code>tini<\/code>. It\u2019s a tiny init binary that handles all this for you.<\/p>\n<pre><code># The right way to handle signals\nRUN apk add --no-cache tini\nENTRYPOINT [\"\/sbin\/tini\", \"--\"]\nCMD [\"node\", \"server.js\"]\n<\/code><\/pre>\n<p>Or, if you\u2019re using a modern version of Docker, just use the <code>--init<\/code> flag. But since you\u2019re likely deploying to Kubernetes or ECS, you need to bake it into the image or ensure your entrypoint script handles signals correctly. Speaking of entrypoint scripts, never use the &#8220;shell form&#8221; of <code>CMD<\/code>.<\/p>\n<pre><code># WRONG: Runs as \/bin\/sh -c \"node server.js\". Signals are lost.\nCMD node server.js\n\n# RIGHT: Runs as \"node server.js\" directly.\nCMD [\"node\", \"server.js\"]\n<\/code><\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Storage_Volumes_vs_Binds\"><\/span>Storage: Volumes vs. Binds<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I\u2019ve seen people lose production data because they didn&#8217;t understand the difference between a bind mount and a volume. A bind mount (<code>-v \/host\/path:\/container\/path<\/code>) is a direct link to a directory on the host. It\u2019s great for development because you can change code on your Mac and see it reflected in the container. In production, it\u2019s a nightmare. It creates a hard dependency on the host&#8217;s file structure. If you move the container to a different node, the data isn&#8217;t there.<\/p>\n<p>Volumes (<code>-v my-data:\/container\/path<\/code>) are managed by Docker. They live in <code>\/var\/lib\/docker\/volumes\/<\/code>. They are abstracted away. But here\u2019s the kicker: Docker never deletes them. You run <code>docker rm -f my-container<\/code>, and that volume stays there forever. Over six months, you\u2019ll accumulate hundreds of gigabytes of &#8220;dangling&#8221; volumes. I\u2019ve seen this take down entire build servers.<\/p>\n<p>The &#8220;Real World&#8221; Gotcha: <code>docker system prune<\/code> is your friend, but <code>docker system prune -a --volumes<\/code> is a nuclear bomb. Use it with caution. I once saw a junior dev run it on a staging server and wipe out the persistent database volumes for the entire QA team. We had backups, but the &#8220;War Room&#8221; was not a fun place to be that afternoon.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_%E2%80%9CReal_World%E2%80%9D_Gotcha_The_OOM_Killer\"><\/span>The &#8220;Real World&#8221; Gotcha: The OOM Killer<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Docker doesn&#8217;t limit memory by default. If your container has a memory leak, it will consume every byte of RAM on the host until the Linux kernel\u2019s Out-Of-Memory (OOM) Killer wakes up. The OOM Killer is a blunt instrument. It looks for the process using the most memory and kills it. Often, that\u2019s not the leaking container. Sometimes it\u2019s the Docker daemon itself. Sometimes it\u2019s the SSH daemon. Suddenly, you can&#8217;t even log into the box to fix it.<\/p>\n<p>Always, always, always set memory limits. And no, <code>--memory=1g<\/code> is not enough. You need to understand the difference between hard limits and soft limits (<code>--memory-reservation<\/code>). A soft limit allows the container to use more RAM if the host has it, but pushes it back down when the host is under pressure. A hard limit kills the container the moment it touches the ceiling.<\/p>\n<pre><code># Example of a responsible container run\ndocker run -d \\\n  --name api-server \\\n  --memory=\"1g\" \\\n  --memory-reservation=\"512m\" \\\n  --cpus=\"1.5\" \\\n  --restart=on-failure:5 \\\n  my-api:v1.2.3\n<\/code><\/pre>\n<p>If you\u2019re running Java, this gets even more complicated. Older versions of the JVM (pre-8u191) don&#8217;t realize they\u2019re in a container. They look at the host&#8217;s total RAM to calculate their heap size. If your host has 64GB of RAM and you limit the container to 2GB, the JVM will try to allocate a 16GB heap and get OOM-killed immediately. Use <code>-XX:+UseContainerSupport<\/code> if you\u2019re stuck on older Java versions, or just upgrade to a version that isn&#8217;t from the Stone Age.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Deep_Dive_The_Container_Runtime_Interface_CRI\"><\/span>Deep Dive: The Container Runtime Interface (CRI)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>If you want to sound like you know what you\u2019re talking about at a cocktail party (or a design doc review), stop saying &#8220;Docker&#8221; when you mean &#8220;the runtime.&#8221; Docker is actually a collection of tools. When you run a container, the Docker daemon (<code>dockerd<\/code>) talks to <code>containerd<\/code>, which then uses <code>runC<\/code> to actually talk to the kernel. <code>runC<\/code> is the thing that creates the namespaces and cgroups.<\/p>\n<p>Why does this matter? Because Kubernetes deprecated Docker as a container runtime years ago. K8s now talks directly to <code>containerd<\/code> via the CRI. If you\u2019re debugging a node in a modern cluster, <code>docker ps<\/code> won&#8217;t work. You have to use <code>crictl ps<\/code>. Understanding this stack helps you realize that Docker is just a UI. The real magic is in the kernel primitives:<\/p>\n<ol>\n<li><b>Namespaces:<\/b> These provide isolation. <code>pid<\/code> (processes), <code>net<\/code> (network), <code>mnt<\/code> (filesystems), <code>uts<\/code> (hostname), <code>ipc<\/code> (inter-process communication).<\/li>\n<li><b>Cgroups (Control Groups):<\/b> These provide resource constraints. CPU, Memory, I\/O, Network bandwidth.<\/li>\n<li><b>Capability Sets:<\/b> These define what the root user can actually do. Even as root, a container usually can&#8217;t load kernel modules or change the system clock unless you give it <code>--privileged<\/code> access (which you shouldn&#8217;t).<\/li>\n<li><b>OverlayFS:<\/b> The copy-on-write filesystem that makes layers possible. It merges the &#8220;lower&#8221; read-only layers with an &#8220;upper&#8221; writable layer.<\/li>\n<\/ol>\n<p>If you want to see what\u2019s actually happening under the hood, try running <code>strace -f -e trace=clone,unshare docker run alpine echo \"hi\"<\/code>. You\u2019ll see the <code>clone()<\/code> syscall with a bunch of flags like <code>CLONE_NEWPID<\/code> and <code>CLONE_NEWNET<\/code>. That\u2019s Docker\u2019s &#8220;secret sauce.&#8221; It\u2019s just a very polished wrapper around <code>clone()<\/code>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Troubleshooting_Like_a_Pro\"><\/span>Troubleshooting Like a Pro<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When a container is failing, <code>docker logs<\/code> is your first stop, but it\u2019s often useless. If the container is crashing before it can even start, the logs will be empty. This is where <code>docker inspect<\/code> comes in. Look at the <code>State<\/code> object. Look for the <code>ExitCode<\/code>. An exit code of <code>137<\/code> means it was OOM-killed. <code>139<\/code> means a segmentation fault. <code>127<\/code> means the command wasn&#8217;t found (usually a PATH issue or a missing dependency in a slim image).<\/p>\n<p>If the container is running but behaving weirdly, don&#8217;t just <code>docker exec -it bash<\/code>. That\u2019s the lazy way. If your image is properly minimized (like Distroless), there won&#8217;t even be a shell to exec into. Instead, use <code>nsenter<\/code>. This tool allows you to enter the namespaces of a running process from the host. It\u2019s like teleporting into the container\u2019s brain without needing a backdoor.<\/p>\n<pre><code># Find the PID of the container's main process\nPID=$(docker inspect --format '{{ .State.Pid }}' my-container)\n\n# Enter the network namespace to run tcpdump from the host\nsudo nsenter -t $PID -n tcpdump -i eth0\n<\/code><\/pre>\n<p>This is how you debug networking issues in production without installing <code>tcpdump<\/code> inside your 10MB production image. You keep your image clean and use the host&#8217;s tools to peer inside.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Registry_Bottleneck\"><\/span>The Registry Bottleneck<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We need to talk about the &#8220;Registry Death Spiral.&#8221; Imagine you have a 500-node cluster. You release a new version of your app. All 500 nodes start pulling a 1GB image at the same time. That\u2019s 500GB of data hitting your registry in a matter of seconds. If you\u2019re using a self-hosted Harbor or a small ECR instance, you\u2019re going to have a bad time. I\u2019ve seen registries fall over, causing deployments to hang and eventually timing out the entire CI\/CD pipeline.<\/p>\n<p>The solution isn&#8217;t &#8220;bigger registry servers.&#8221; The solution is better image management. Use <code>docker-squash<\/code> or multi-stage builds to keep images under 200MB. Use a P2P image distribution tool like Uber\u2019s Kraken or Alibaba\u2019s Dragonfly if you\u2019re at massive scale. But for most of us, just being smart about layers is enough. If your &#8220;base&#8221; layer (the one with all your OS libraries) changes every day, you\u2019re doing it wrong. That layer should be stable for weeks, so nodes only have to pull the tiny &#8220;app&#8221; layer on each deploy.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Wrap-up\"><\/span>The Wrap-up<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Docker is a tool, not a philosophy. It\u2019s a way to package a process so it runs predictably, but it doesn&#8217;t absolve you from the responsibility of knowing how Linux works. Stop building bloated images, start using multi-stage builds, respect the PID 1 signal handling, and for the love of all that is holy, stop running your containers as root. Docker isn&#8217;t magic; it&#8217;s just <code>tar<\/code> files and <code>iptables<\/code> rules. Treat it with the skepticism it deserves, and it might actually work when you need it to.<\/p>\n<p>Stop reading tutorials and start reading the <code>man<\/code> pages for <code>namespaces<\/code> and <code>cgroups<\/code>. That\u2019s where the real senior-level knowledge lives.<\/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\/what-is-kubernetes-a-complete-guide-to-orchestration\/\">What Is Kubernetes A Complete Guide To Orchestration<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-10-tips-for-faster-leaner-images\/\">Docker Best Practices 10 Tips For Faster Leaner Images<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/microsoft-azure-a-complete-guide-to-cloud-computing\/\">Microsoft Azure A Complete Guide To Cloud Computing<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Docker is Not a Virtual Machine, and Your Images are Too Fat It was 3:14 AM on a Tuesday in 2017. I was the &#8220;on-call hero&#8221; for a fintech startup that shall remain nameless. We had a monolithic Python API that we\u2019d recently &#8220;containerized&#8221; to be modern. I pushed a change to the master branch, &#8230; <a title=\"Docker Tutorial: Build, Ship, and Run Apps Anywhere\" class=\"read-more\" href=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\" aria-label=\"Read more  on Docker Tutorial: Build, Ship, and Run Apps Anywhere\">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-4895","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 Tutorial: Build, Ship, and Run Apps Anywhere - 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-tutorial-build-ship-and-run-apps-anywhere\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Docker Tutorial: Build, Ship, and Run Apps Anywhere - ITSupportWale\" \/>\n<meta property=\"og:description\" content=\"Docker is Not a Virtual Machine, and Your Images are Too Fat It was 3:14 AM on a Tuesday in 2017. I was the &#8220;on-call hero&#8221; for a fintech startup that shall remain nameless. We had a monolithic Python API that we\u2019d recently &#8220;containerized&#8221; to be modern. I pushed a change to the master branch, ... Read more\" \/>\n<meta property=\"og:url\" content=\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\" \/>\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-10-01T20:17:18+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=\"14 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\"},\"author\":{\"name\":\"Techie\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\"},\"headline\":\"Docker Tutorial: Build, Ship, and Run Apps Anywhere\",\"datePublished\":\"2026-10-01T20:17:18+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\"},\"wordCount\":2429,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\",\"name\":\"Docker Tutorial: Build, Ship, and Run Apps Anywhere - ITSupportWale\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\"},\"datePublished\":\"2026-10-01T20:17:18+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/itsupportwale.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Docker Tutorial: Build, Ship, and Run Apps Anywhere\"}]},{\"@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 Tutorial: Build, Ship, and Run Apps Anywhere - 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-tutorial-build-ship-and-run-apps-anywhere\/","og_locale":"en_US","og_type":"article","og_title":"Docker Tutorial: Build, Ship, and Run Apps Anywhere - ITSupportWale","og_description":"Docker is Not a Virtual Machine, and Your Images are Too Fat It was 3:14 AM on a Tuesday in 2017. I was the &#8220;on-call hero&#8221; for a fintech startup that shall remain nameless. We had a monolithic Python API that we\u2019d recently &#8220;containerized&#8221; to be modern. I pushed a change to the master branch, ... Read more","og_url":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/","og_site_name":"ITSupportWale","article_publisher":"https:\/\/www.facebook.com\/Itsupportwale-298547177495978","article_published_time":"2026-10-01T20:17:18+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":"14 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#article","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/"},"author":{"name":"Techie","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d"},"headline":"Docker Tutorial: Build, Ship, and Run Apps Anywhere","datePublished":"2026-10-01T20:17:18+00:00","mainEntityOfPage":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/"},"wordCount":2429,"commentCount":0,"publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/","url":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/","name":"Docker Tutorial: Build, Ship, and Run Apps Anywhere - ITSupportWale","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/#website"},"datePublished":"2026-10-01T20:17:18+00:00","breadcrumb":{"@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/itsupportwale.com\/blog\/docker-tutorial-build-ship-and-run-apps-anywhere\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/itsupportwale.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Docker Tutorial: Build, Ship, and Run Apps Anywhere"}]},{"@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\/4895","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=4895"}],"version-history":[{"count":0,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4895\/revisions"}],"wp:attachment":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/media?parent=4895"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/categories?post=4895"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/tags?post=4895"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}