<?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: John Afariogun</title>
    <description>The latest articles on DEV Community by John Afariogun (@john_afariogun_e2351c78af).</description>
    <link>https://dev.to/john_afariogun_e2351c78af</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3568632%2F860220c4-cc82-4e44-acd9-63181e677f66.png</url>
      <title>DEV Community: John Afariogun</title>
      <link>https://dev.to/john_afariogun_e2351c78af</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/john_afariogun_e2351c78af"/>
    <language>en</language>
    <item>
      <title>The Hidden Life of a Container: A Complete Lifecycle</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Thu, 21 May 2026 12:37:05 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/the-hidden-life-of-a-container-a-complete-lifecycle-3mod</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/the-hidden-life-of-a-container-a-complete-lifecycle-3mod</guid>
      <description>&lt;p&gt;&lt;em&gt;The anatomy of a container tells you what the walls are made of. This &lt;a href="https://www.linkedin.com/pulse/deep-dive-docker-what-ticks-under-hood-john-afariogun-k3xye" rel="noopener noreferrer"&gt;article&lt;/a&gt; tells you when they go up, what happens inside them, and what the kernel does the moment they come down.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the previous &lt;a href="https://www.linkedin.com/pulse/deep-dive-docker-what-ticks-under-hood-john-afariogun-k3xye" rel="noopener noreferrer"&gt;article&lt;/a&gt;, we dissected a running container at a single moment in time: the namespaces building the illusion of isolation, the cgroups enforcing the resource constitution, OverlayFS merging layers into a coherent filesystem, the veth pair wiring the container into the network. A cross-section. A photograph.&lt;/p&gt;

&lt;p&gt;But a photograph doesn't tell you how the subject got there, or what happens after the shutter closes.&lt;/p&gt;

&lt;p&gt;That's what this piece is for. We'll follow a single container from before it exists to after it's gone — and at each transition, we'll look at what actually changes at the kernel level. The machinery is the same as before. Now we watch it move.&lt;/p&gt;

&lt;p&gt;We'll also cover the three places where production systems quietly break in ways that monitoring won't catch until it's too late: signal handling, zombie processes, and the OOM killer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before the Container: The Image as a Promise
&lt;/h2&gt;

&lt;p&gt;Before anything runs, the runtime needs something to run from. An image isn't a virtual machine disk or a binary blob — it's a stack of read-only filesystem layers, each one a delta on the last, stored under &lt;code&gt;/var/lib/docker/overlay2/&lt;/code&gt; and identified by SHA256 digest rather than name.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;docker pull&lt;/code&gt;, the daemon fetches a &lt;em&gt;manifest&lt;/em&gt; first: a JSON document listing every layer by digest. It then checks which of those digests already exist locally and downloads only the missing ones. This is why pulling a new version of an image that shares a base layer with one you already have takes seconds — the common layers are already there. The registry protocol is content-addressed, which means layers are inherently deduplicated across every image on the host that uses them.&lt;/p&gt;

&lt;p&gt;Each downloaded layer is verified against its digest before being committed to the store. A tampered or corrupted layer fails this check before it touches anything.&lt;/p&gt;

&lt;p&gt;The image at rest is inert. It's a promise of what the container's filesystem will look like when it starts — nothing more. The OverlayFS that merges those layers into a live, writable filesystem doesn't exist yet. That comes with create.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: &lt;code&gt;docker create&lt;/code&gt; — Assembling the Environment
&lt;/h2&gt;

&lt;p&gt;Here is something most engineers don't know: &lt;code&gt;docker run&lt;/code&gt; is not a single operation. It is &lt;code&gt;docker create&lt;/code&gt; followed immediately by &lt;code&gt;docker start&lt;/code&gt;. Knowing this distinction is practically useful — but more importantly, it clarifies &lt;em&gt;when&lt;/em&gt; each piece of kernel infrastructure comes into existence.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker create&lt;/code&gt; builds the complete environment the container will inhabit. Nothing executes. No process runs. It is pure allocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The OverlayFS mount.&lt;/strong&gt; The runtime creates a new &lt;em&gt;upperdir&lt;/em&gt; — an empty, writable directory unique to this container instance. The image's read-only layers become the &lt;em&gt;lowerdir&lt;/em&gt;. The OverlayFS is configured to present both as a single unified filesystem: reads fall through to the lower layers, writes land in the upper layer, and the container will see a seamless root filesystem when it starts. The layers you read about in the anatomy piece are present here. The writable layer that captures everything your container does is created here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The namespace allocation.&lt;/strong&gt; The six namespaces — PID, NET, MNT, IPC, UTS, USER — are prepared. At this point they're reserved but not yet active. There's nothing to isolate, so isolation hasn't started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cgroup hierarchy.&lt;/strong&gt; A cgroup is created for this container under &lt;code&gt;/sys/fs/cgroup/&lt;/code&gt;. The memory limit from &lt;code&gt;--memory 512m&lt;/code&gt; and the CPU quota from &lt;code&gt;--cpus 0.5&lt;/code&gt; are written into the cgroup's control files right now. The cgroup exists. The limits are set. But with no processes inside it, it enforces nothing — a constitution with no citizens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The network configuration.&lt;/strong&gt; The veth pair is created: one end destined for the container's network namespace, one end connecting to the &lt;code&gt;docker0&lt;/code&gt; bridge on the host. An IP is assigned. NAT rules are written to iptables for port mappings. The virtual cable exists, but nothing is plugged in on the container end yet.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;docker create&lt;/code&gt;, the container is a complete kernel data structure — every wall built, every rule written, every wire run — with no tenant. Its state is &lt;code&gt;"created"&lt;/code&gt;. Nothing has been charged to the CPU. Nothing has touched the writable layer.&lt;/p&gt;

&lt;p&gt;The practical implication: you can pre-warm containers during off-peak hours and start them in milliseconds when demand arrives. The expensive work of building the environment is already done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: &lt;code&gt;docker start&lt;/code&gt; — Crossing the Threshold
&lt;/h2&gt;

&lt;p&gt;This is the moment the environment becomes inhabited. Three components work in sequence: &lt;code&gt;dockerd&lt;/code&gt;, &lt;code&gt;containerd&lt;/code&gt;, and &lt;code&gt;containerd-shim&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dockerd&lt;/code&gt; instructs &lt;code&gt;containerd&lt;/code&gt; to start the container. &lt;code&gt;containerd&lt;/code&gt; forks a &lt;code&gt;containerd-shim&lt;/code&gt; — the small, dedicated supervisor we covered in the daemon article — which then calls &lt;code&gt;runc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;runc&lt;/code&gt; is where the kernel call happens. It takes the container's prepared configuration and calls &lt;code&gt;clone()&lt;/code&gt; with the namespace flags: &lt;code&gt;CLONE_NEWPID&lt;/code&gt;, &lt;code&gt;CLONE_NEWNET&lt;/code&gt;, &lt;code&gt;CLONE_NEWNS&lt;/code&gt;, and the rest. The namespaces activate. The OverlayFS is mounted. The cgroup begins accounting. The veth pair connects. Then &lt;code&gt;runc&lt;/code&gt; calls &lt;code&gt;execve()&lt;/code&gt; with whatever you specified as &lt;code&gt;ENTRYPOINT&lt;/code&gt; or &lt;code&gt;CMD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That process — your application — becomes &lt;strong&gt;PID 1&lt;/strong&gt; inside the container.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;execve()&lt;/code&gt; returns, &lt;code&gt;runc&lt;/code&gt; exits. It has done its job. The &lt;code&gt;containerd-shim&lt;/code&gt; stays behind, holding the container's stdin/stdout file descriptors and watching PID 1. If &lt;code&gt;dockerd&lt;/code&gt; crashes, the shim keeps the container alive — which is the architecture we walked through in detail previously.&lt;/p&gt;

&lt;p&gt;The moment &lt;code&gt;docker start&lt;/code&gt; completes, everything the anatomy article described is live and active: the isolated process tree, the private network stack, the merged filesystem, the cgroup enforcing limits. The photograph is now a film.&lt;/p&gt;

&lt;p&gt;And this is the first place things go quietly wrong. PID 1 carries obligations that your application almost certainly doesn't know about. We'll come back to that shortly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3: Running — What the Commands Actually Do
&lt;/h2&gt;

&lt;p&gt;A running container looks opaque from the outside. Under the hood, every &lt;code&gt;docker&lt;/code&gt; command in this state is a thin wrapper over kernel operations you already understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker exec&lt;/code&gt;&lt;/strong&gt; is the most commonly misunderstood. When you run &lt;code&gt;docker exec -it myapp-prod /bin/sh&lt;/code&gt;, Docker doesn't create a new container. It calls &lt;code&gt;setns()&lt;/code&gt; — a system call that lets a process join &lt;em&gt;existing&lt;/em&gt; namespaces. Your shell enters the container's PID and network namespaces and sees the same environment the application sees. Nothing new is created. You're walking into an existing apartment, not building a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker logs&lt;/code&gt;&lt;/strong&gt; reads from the file descriptors the &lt;code&gt;containerd-shim&lt;/code&gt; has been holding since start. The shim inherited the container's stdout and stderr pipes and has been buffering them since the first byte was written. No daemon magic — the shim is just a persistent pipe holder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker stats&lt;/code&gt;&lt;/strong&gt; reads cgroup control files directly: &lt;code&gt;/sys/fs/cgroup/memory.current&lt;/code&gt; for RSS, CPU accounting files for usage. The numbers you see in the terminal are the same numbers the kernel is maintaining to enforce your limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker pause&lt;/code&gt;&lt;/strong&gt; is the most elegant operation in this set. It uses the cgroup freezer subsystem to send &lt;code&gt;SIGSTOP&lt;/code&gt; to every process in the container simultaneously — not one at a time, but atomically, via the cgroup. They freeze mid-instruction with no opportunity to catch the signal or react. &lt;code&gt;docker unpause&lt;/code&gt; sends &lt;code&gt;SIGCONT&lt;/code&gt; through the same path and they resume exactly where they stopped. In-flight operations, open sockets, memory contents — all preserved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: &lt;code&gt;docker stop&lt;/code&gt; — The Two-Phase Shutdown
&lt;/h2&gt;

&lt;p&gt;This is where most production problems originate, and where the PID 1 decision made at start time comes due.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker stop&lt;/code&gt; is a two-phase protocol. Phase one: &lt;code&gt;SIGTERM&lt;/code&gt; is sent to PID 1. The application has a grace period — ten seconds by default, adjustable with &lt;code&gt;-t&lt;/code&gt; — to finish what it's doing and exit cleanly. Close database connections. Drain in-flight requests. Flush logs. Phase two: if PID 1 is still alive when the timer expires, &lt;code&gt;SIGKILL&lt;/code&gt; is sent. There is no negotiation with SIGKILL. The kernel terminates the process immediately and unconditionally.&lt;/p&gt;

&lt;p&gt;The failure mode that bites teams most often: &lt;strong&gt;the shell wrapper&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Many containers are launched like this: &lt;code&gt;CMD ["sh", "-c", "node server.js"]&lt;/code&gt;. The shell becomes PID 1. When Docker sends SIGTERM to PID 1, the shell receives it — and by default, shells do not forward signals to their children. Your Node.js process never sees SIGTERM. Ten seconds pass. SIGKILL arrives. The server dies mid-request, connections drop, and the exit code suggests a crash rather than a clean shutdown.&lt;/p&gt;

&lt;p&gt;The fix is to make your application PID 1 directly. Use the exec form: &lt;code&gt;CMD ["node", "server.js"]&lt;/code&gt;. Or, if you need a shell script for setup, end it with &lt;code&gt;exec node server.js&lt;/code&gt; — the &lt;code&gt;exec&lt;/code&gt; replaces the shell process rather than forking a child, so your application inherits PID 1.&lt;/p&gt;

&lt;p&gt;Ten seconds is also almost always too short. An application with database connection pools to drain and requests to finish needs more. Set &lt;code&gt;-t 30&lt;/code&gt; as a floor. For services handling long-lived connections, &lt;code&gt;-t 60&lt;/code&gt; or higher is appropriate. The grace period is your only window for a clean shutdown — size it honestly.&lt;/p&gt;

&lt;p&gt;After a successful stop, the container's state becomes &lt;code&gt;"exited"&lt;/code&gt;. Crucially, the writable OverlayFS layer is preserved — every file the application created or modified is still there. The container can be restarted with &lt;code&gt;docker start&lt;/code&gt; and will resume from exactly the filesystem state it stopped in.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Danger Zone: Zombie Processes
&lt;/h2&gt;

&lt;p&gt;On a normal Linux host, PID 1 is the init system. One of init's core responsibilities — one so fundamental it's baked into the kernel's design — is &lt;em&gt;reaping&lt;/em&gt; dead child processes. When a process exits, it doesn't fully disappear. Its kernel entry stays open, holding its exit code, until its parent calls &lt;code&gt;wait()&lt;/code&gt; to collect that code. Until then, it sits in state &lt;code&gt;Z&lt;/code&gt;: a zombie. Dead, but not yet buried.&lt;/p&gt;

&lt;p&gt;Init's job is to call &lt;code&gt;wait()&lt;/code&gt; on every process that loses its parent, so the kernel slot gets freed. Without init doing this, zombie entries accumulate indefinitely.&lt;/p&gt;

&lt;p&gt;In a container, your application is PID 1. And your application — a web server, an API, a worker — was written to serve requests, not to supervise processes. It almost certainly never calls &lt;code&gt;wait()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The scenario: your web server forks a worker to handle a long request. The worker finishes and calls &lt;code&gt;exit()&lt;/code&gt;. It waits for its parent to acknowledge the exit. The parent never does. The worker's kernel entry sits in &lt;code&gt;Z&lt;/code&gt; state. Do this at scale, over days of production traffic, and the PID table fills. Eventually &lt;code&gt;fork()&lt;/code&gt; fails — the container cannot spawn new processes — and the application breaks in a way that looks nothing like its actual cause.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker stats&lt;/code&gt; will show the container as healthy throughout. CPU and memory are fine. The zombie count isn't surfaced by any standard metric.&lt;/p&gt;

&lt;p&gt;The fix is a proper init process sitting in front of your application. &lt;code&gt;tini&lt;/code&gt; is the canonical choice — purpose-built for containers, less than a thousand lines of C, does one thing: reap zombies and forward signals correctly.&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;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; tini
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/tini", "--", "/usr/local/bin/myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't want to touch the image, &lt;code&gt;docker run --init&lt;/code&gt; injects a bundled init binary automatically. Use one or the other on every production container, as a default, without exception.&lt;/p&gt;

&lt;p&gt;To check for zombies in a running container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;myapp-prod ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;' Z '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any output means you have a problem. The fix is &lt;code&gt;tini&lt;/code&gt;. The time to add it is before the deployment, not during the incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Danger Zone: The OOM Killer
&lt;/h2&gt;

&lt;p&gt;The cgroup memory limit — set at create time, written to &lt;code&gt;memory.max&lt;/code&gt; in cgroup v2 — is not a suggestion. When a container's resident memory usage reaches that ceiling, the kernel's Out-Of-Memory killer activates.&lt;/p&gt;

&lt;p&gt;The OOM killer scores every process in the cgroup by a "badness" calculation: how much memory the process uses relative to total system memory, adjusted by that process's &lt;code&gt;oom_score_adj&lt;/code&gt; value. The highest scorer is sent &lt;code&gt;SIGKILL&lt;/code&gt;. In a single-process container, there is exactly one candidate. PID 1 always loses.&lt;/p&gt;

&lt;p&gt;The critical distinction from &lt;code&gt;docker stop&lt;/code&gt;: the OOM killer sends no SIGTERM. There is no grace period. No warning, no chance to flush state, close connections, or write a final log line. The process simply stops existing. Docker records this as &lt;code&gt;OOMKilled: true&lt;/code&gt; in the container's inspect output:&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 myapp-prod &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.OOMKilled}}'&lt;/span&gt;
&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel also logs it to &lt;code&gt;dmesg&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;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"oom&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;killed process"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mitigation works at two levels. The first is setting a &lt;em&gt;soft limit&lt;/em&gt; below the hard limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--memory&lt;/span&gt; 512m &lt;span class="nt"&gt;--memory-reservation&lt;/span&gt; 400m myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--memory-reservation&lt;/code&gt; tells the kernel to apply gentle memory pressure — nudging the container's garbage collector, encouraging page reclamation — before the hard ceiling is reached. It's an early warning system rather than a wall. The container can still exceed the reservation temporarily; only &lt;code&gt;--memory&lt;/code&gt; is the hard stop.&lt;/p&gt;

&lt;p&gt;The second level is treating OOM kills as bugs, not tuning parameters. A container that is repeatedly OOM-killed has either a memory leak or limits set below its actual working set. Raising the limit is a short-term fix. Finding the leak is the real work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5: &lt;code&gt;docker rm&lt;/code&gt; — Dismantling the Environment
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker rm&lt;/code&gt; is the exact inverse of &lt;code&gt;docker create&lt;/code&gt;. Every kernel structure that create allocated is released in reverse.&lt;/p&gt;

&lt;p&gt;The OverlayFS upper layer is deleted. Every file your application created, modified, or deleted since the container started — the entire writable history — is gone. If your application wrote logs or state directly to the container filesystem, it's unrecoverable. This is the reason containerised applications should write persistent data to mounted volumes, not the container filesystem: &lt;code&gt;docker rm&lt;/code&gt; treats the writable layer as ephemeral by design.&lt;/p&gt;

&lt;p&gt;The veth pair is removed. The IP address returns to Docker's pool. The iptables NAT rules for port mappings are deleted. The network configuration from create is fully unwound.&lt;/p&gt;

&lt;p&gt;The cgroup hierarchy is destroyed. The entries under &lt;code&gt;/sys/fs/cgroup/&lt;/code&gt; that tracked this container's memory and CPU are removed. The kernel stops accounting for this container entirely.&lt;/p&gt;

&lt;p&gt;What survives: the image layers. The read-only lowerdir that formed the container's base filesystem is untouched — available immediately for the next container. And named volumes survive unless you pass &lt;code&gt;--volumes&lt;/code&gt; to &lt;code&gt;docker rm&lt;/code&gt;. Persistent data stored in volumes outlives the container that created it, which is the intended design.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture
&lt;/h2&gt;

&lt;p&gt;The anatomy article showed you the ingredients. This one showed you the recipe — the same namespaces, cgroups, OverlayFS, and veth pairs assembled in sequence, operated during a container's life, and dismantled at its end.&lt;/p&gt;

&lt;p&gt;The mental model to carry forward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create is allocation, start is execution.&lt;/strong&gt; Every kernel structure — namespaces, cgroups, OverlayFS, veth pairs — exists before your application runs a single instruction. This is why pre-warming works and why restart is fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PID 1 is a contract, not just a position.&lt;/strong&gt; The kernel gives PID 1 responsibilities that init normally handles: signal forwarding and zombie reaping. If your application holds that position, it inherits those responsibilities. Use &lt;code&gt;tini&lt;/code&gt; or &lt;code&gt;--init&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIGTERM is your only warning.&lt;/strong&gt; The grace period between SIGTERM and SIGKILL is the only window you get for a clean shutdown. Your application must handle the signal, and the window must be sized for your actual workload. Ten seconds is almost always too short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The OOM killer gives no warning at all.&lt;/strong&gt; There is no SIGTERM, no grace period, no log line from your application. If &lt;code&gt;OOMKilled: true&lt;/code&gt; appears in production, treat it as a bug — because it is one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The writable layer is temporary.&lt;/strong&gt; &lt;code&gt;docker rm&lt;/code&gt; destroys it completely. Write persistent state to volumes. Treat the container filesystem as scratch space that disappears when the container does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run&lt;/code&gt; looks like a single command. It is actually the sequential execution of eight distinct kernel operations, each with observable state and specific failure modes. The engineers who debug containers fastest aren't the ones who know the most flags — they're the ones who can trace a problem back to the operation where the kernel did something unexpected.&lt;/p&gt;

&lt;p&gt;Now you can.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of an ongoing series on container internals. Previous: &lt;a href="https://www.linkedin.com/pulse/deep-dive-docker-what-ticks-under-hood-john-afariogun-k3xye" rel="noopener noreferrer"&gt;A Deep Dive into Docker: What Ticks Under the Hood?&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>containers</category>
      <category>linux</category>
      <category>networking</category>
    </item>
    <item>
      <title>The Middle Child Syndrome: Why Your Frontend might not be able to Talk to it's Docker Siblings</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Fri, 20 Feb 2026 15:58:08 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/the-middle-child-syndrome-why-your-frontend-might-not-be-able-to-talk-to-its-docker-siblings-4f99</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/the-middle-child-syndrome-why-your-frontend-might-not-be-able-to-talk-to-its-docker-siblings-4f99</guid>
      <description>&lt;p&gt;&lt;em&gt;Containerizing full-stack apps reveals a harsh reality—your React frontend is the awkward middle child that can't speak to its Docker siblings.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Containerizing a full-stack application is a rite of passage for every DevOps-leaning engineer. You successfully get your Node.js backend talking to PostgreSQL, your Python ML service crunching data, and Redis caching everything in between.&lt;/p&gt;

&lt;p&gt;But then, the "Middle Child" enters the room: &lt;strong&gt;The Frontend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Despite being part of the &lt;code&gt;docker-compose.yml&lt;/code&gt; family, the frontend often feels isolated, unable to speak the same internal language as its Docker siblings. Here's why it happens and how to solve it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Family Reunion That Excluded Frontend
&lt;/h2&gt;

&lt;p&gt;I recently orchestrated a multi-service e-commerce app with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend (Node.js/Express)    ✅ Connected
ML Service (Python/Flask)    ✅ Connected  
PostgreSQL Database          ✅ Connected
Redis Cache                  ✅ Connected
React Frontend               ❌ Left outside in the cold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inside the Docker bridge network, life was beautiful.&lt;/strong&gt; My backend could reach the ML service simply by using the service name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inside the backend container&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://ml-service:5000/recommendations/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Works perfectly!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Docker DNS handles the heavy lifting. Services on the same bridge network are family.&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;# docker-compose.yml - Happy family networking&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&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="nv"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;ml-service&lt;/span&gt;&lt;span class="pi"&gt;:&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="nv"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&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="nv"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&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="nv"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;]&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;app-network&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;h2&gt;
  
  
  Frontend Got Kicked Out (The Harsh Reality)
&lt;/h2&gt;

&lt;p&gt;Then I tried the same thing from my React frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React frontend trying to join the family...&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://backend:8080/api/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sibling rivalry:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Browser Console:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;net::ERR_NAME_NOT_RESOLVED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Did This Fail?
&lt;/h3&gt;

&lt;p&gt;The "Middle Child Syndrome" stems from a fundamental misunderstanding of &lt;strong&gt;where the code actually runs&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Backend code&lt;/strong&gt; runs &lt;em&gt;inside&lt;/em&gt; the Docker container → uses Docker's internal DNS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend code&lt;/strong&gt; is &lt;em&gt;delivered&lt;/em&gt; by Docker, but &lt;strong&gt;executes in the user's browser&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The browser lives on your host machine&lt;/strong&gt; (your device), not inside the Docker bridge network. Your device's DNS has no idea what &lt;code&gt;http://backend&lt;/code&gt; is. The Docker network is a private club your browser doesn't have membership for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ←─── HTTP ───► ??? ──── Docker Network ───► Services
  │                              (backend, ml, db, redis)
  │
  └── Can't reach Docker DNS directly ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Solutions: Pick Your Poison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Expose Everything&lt;/strong&gt; (Security Nightmare ⚠️)
&lt;/h3&gt;

&lt;p&gt;The quickest fix is to expose every service to your host machine:&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&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;8080:8080"&lt;/span&gt;  &lt;span class="c1"&gt;# Now public on localhost&lt;/span&gt;
  &lt;span class="na"&gt;ml-service&lt;/span&gt;&lt;span class="pi"&gt;:&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;5000:5000"&lt;/span&gt;  &lt;span class="c1"&gt;# Exposed to internet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React calls &lt;code&gt;http://localhost:8080&lt;/code&gt;, also calls &lt;code&gt;http://localhost:5000&lt;/code&gt; ✅ Works, but you've just exposed all your internal services to the entire internet. In production, this is a massive security "no-go."&lt;/p&gt;




&lt;h3&gt;
  
  
  2. &lt;strong&gt;Backend Proxy&lt;/strong&gt; (Secure &amp;amp; Simple)
&lt;/h3&gt;

&lt;p&gt;Route frontend requests through your backend, which &lt;em&gt;can&lt;/em&gt; access Docker DNS:&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&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;8080:8080"&lt;/span&gt;  &lt;span class="c1"&gt;# Single exposed port&lt;/span&gt;
  &lt;span class="na"&gt;ml-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports exposed - internal only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend proxies ML requests:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// backend/server.js&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/ml/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mlUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`http://ml-service:5000/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mlUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React calls &lt;code&gt;/api/ml/recommendations&lt;/code&gt; → Backend proxies to &lt;code&gt;ml-service:5000&lt;/code&gt; ✅ Secure + elegant.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. &lt;strong&gt;Nginx Reverse Proxy&lt;/strong&gt; (Production Ready)
&lt;/h3&gt;

&lt;p&gt;This is the most architecturally sound approach. Put a "Gatekeeper" in front of your family:&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;nginx&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;nginx:alpine&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;80:80"&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;./nginx.conf:/etc/nginx/nginx.conf&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports exposed&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports exposed&lt;/span&gt;

  &lt;span class="na"&gt;ml-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports exposed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;nginx.conf:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend:8080/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/ml/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://ml-service:5000/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://frontend:3000/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your frontend only talks to &lt;strong&gt;one&lt;/strong&gt; place: the Nginx port. Nginx lives &lt;em&gt;inside&lt;/em&gt; the Docker network and handles routing to all siblings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ←─── HTTP ───► Nginx (port 80) ──── Docker Network ───► Services ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Root Cause: Network Architecture Mismatch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker bridge networks solve:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Service-to-service communication&lt;/li&gt;
&lt;li&gt;✅ Container DNS resolution
&lt;/li&gt;
&lt;li&gt;❌ Browser-to-container (without port mapping or proxy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix requires understanding execution context:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code Location&lt;/th&gt;
&lt;th&gt;Runs Where?&lt;/th&gt;
&lt;th&gt;Can Use Docker DNS?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;backend/server.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inside container&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ml-service/app.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inside container&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend/src/App.jsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;In browser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Lessons Learned (The Hard Way)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Execution Context is King&lt;/strong&gt; - Always ask: "Where is this code &lt;em&gt;actually&lt;/em&gt; running?" If it's a &lt;code&gt;.jsx&lt;/code&gt; file, it runs in the browser, not Docker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One Ingress Point&lt;/strong&gt; - Avoid "Swiss Cheese" security. Expose one port (80/443) and route everything internally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment Variables Save Lives:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CORS is Your Friend&lt;/strong&gt; - Configure properly on all services that Nginx proxies:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Complete Working Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&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;nginx&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;nginx:alpine&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;80:80"&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;./nginx.conf:/etc/nginx/nginx.conf&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;frontend&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="s"&gt;./frontend&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports - accessed via nginx&lt;/span&gt;

  &lt;span class="na"&gt;backend&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="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
    &lt;span class="c1"&gt;# No ports - accessed via nginx&lt;/span&gt;

  &lt;span class="na"&gt;ml-service&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="s"&gt;./ml-service&lt;/span&gt;
    &lt;span class="c1"&gt;# Internal only&lt;/span&gt;

  &lt;span class="na"&gt;postgres&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;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shopmicro&lt;/span&gt;

  &lt;span class="na"&gt;redis&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:alpine&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;default&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;The frontend middle child finally found its place&lt;/strong&gt; - behind a proxy, secure, and talking to all its Docker siblings through proper networking architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;In my next post, I'll share how I took this &lt;strong&gt;ShopMicro&lt;/strong&gt; architecture and scaled it into a &lt;strong&gt;Kubernetes cluster&lt;/strong&gt; with Ingress controllers—where the networking gets even more "fun."&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>fullstack</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>🔔 Notifycli: A Lightweight Go CLI for Firebase Push Notifications</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Fri, 05 Dec 2025 08:28:00 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/notifycli-a-lightweight-go-cli-for-firebase-push-notifications-74b</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/notifycli-a-lightweight-go-cli-for-firebase-push-notifications-74b</guid>
      <description>&lt;p&gt;Need to send push notifications to your mobile or web clients — from the command line, a backend script, or a CI/CD pipeline — without building a full backend server?&lt;br&gt;
&lt;strong&gt;Notifycli&lt;/strong&gt; is a simple, Go-based CLI that does exactly that: use Firebase Cloud Messaging (FCM) credentials + a device token to send push notifications.&lt;/p&gt;

&lt;p&gt;In this post, I unpack the code, show installation and usage, and highlight how it's built — so you can use or extend it for your own projects.&lt;/p&gt;


&lt;h3&gt;
  
  
  What is Notifycli?
&lt;/h3&gt;

&lt;p&gt;According to the project README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifycli is a “Go CLI for sending push notifications to mobile and web devices using Firebase Cloud Messaging.” (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;It supports sending to a single device (via FCM device token), and allows custom data payloads — e.g. a title, body, optional URL — which your app can use for deep linking or redirection. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;It uses a clean project structure based on the popular CLI framework Cobra for subcommands and flag parsing. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's a standalone binary (no heavy deps, no web server), it works well for automation: cron jobs, server scripts, deployment hooks, or any backend that just needs to “fire-and-forget” an FCM push.&lt;/p&gt;


&lt;h3&gt;
  
  
  Project Structure &amp;amp; Architecture
&lt;/h3&gt;

&lt;p&gt;Here’s how the repo is structured: (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notifycli/
 ├── cmd/
 │    ├── root.go         # CLI root command definitions
 │    └── notify.go       # “notify” subcommand: sends a notification
 ├── internal/
 │    └── firebase/
 │         └── client.go  # Firebase FCM client wrapper
 ├── main.go              # Entry point
 ├── go.mod / go.sum      # Module definitions
 ├── test/                # (optional) tests
 └── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cmd/&lt;/code&gt;: Uses Cobra to define commands and flags.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;internal/firebase/client.go&lt;/code&gt;: Encapsulates FCM initialization, authentication using a Firebase service-account JSON key, and the logic to send notifications via FCM REST API. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Minimal external dependencies — this keeps the binary lightweight and portable.&lt;/li&gt;
&lt;li&gt;Easy to integrate with scripts, cron jobs, CI/CD pipelines — you just call &lt;code&gt;notifcli notify ...&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  How to Build &amp;amp; Use
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Clone &amp;amp; Build
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/johnafariogun/Notifycli.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Notifycli
go build &lt;span class="nt"&gt;-o&lt;/span&gt; notifcli
&lt;span class="c"&gt;# optionally:&lt;/span&gt;
go &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Provide Firebase Credentials
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Go to your Firebase project → &lt;strong&gt;Project Settings → Service Accounts → Generate new private key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;serviceAccount.json&lt;/code&gt; and place it in your project root (or anywhere you like, but pass its path with &lt;code&gt;--creds&lt;/code&gt;). (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Send a Notification via CLI
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;notifcli notify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;DEVICE_FCM_TOKEN&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Deployment Successful"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Your service is live."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--creds&lt;/span&gt; /path/to/serviceAccount.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional flag: &lt;code&gt;--url&lt;/code&gt;, to include a link in the data payload (useful for app deep links or redirect URLs). (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That’s all — a simple, one-liner to trigger a push notification from anywhere.&lt;/p&gt;




&lt;h3&gt;
  
  
  Under the Hood: What’s Going On
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The CLI parses flags (token, title, body, url, creds) via Cobra in &lt;code&gt;cmd/notify.go&lt;/code&gt;. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;internal/firebase/client.go&lt;/code&gt; module: initializes a Firebase app using the provided service account, constructs the FCM message (with title/body, and optional data payload containing &lt;code&gt;link&lt;/code&gt;), and sends the request to FCM via HTTPS. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Error handling covers cases like missing credentials, invalid FCM token, or network failures. The CLI prints descriptive error messages. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Because it's a native Go binary, it’s cross-platform (Linux, macOS, etc.) and doesn’t require runtime dependencies beyond Go’s standard library + net/http.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why This Tool Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity &amp;amp; Speed&lt;/strong&gt;: No need to build a full backend just to send notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation-friendly&lt;/strong&gt;: Great for deployment scripts, CI/CD, cron jobs, or server-side alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable&lt;/strong&gt;: Build once, deploy anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible&lt;/strong&gt;: You can extend &lt;code&gt;internal/firebase/client.go&lt;/code&gt; to support more advanced FCM features — e.g. topic messages, batch sends, custom data payloads, image notifications. The architecture is clean and modular. (&lt;a href="https://github.com/johnafariogun/Notifycli.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Wrap-up &amp;amp; Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Notifycli is a great example of how a small, focused tool can do one thing well — in this case: send FCM push notifications — and integrate easily into scripts, automation, or backend workflows.&lt;br&gt;
Its clean Go + Cobra structure and minimal dependencies make it perfect for developers or DevOps workflows that don’t need a full backend server just for notifications.&lt;/p&gt;

&lt;p&gt;If you need a lightweight, scriptable, cross-platform tool for mobile/web push notifications — especially useful for deployment alerts, status updates, or automation — give Notifycli a try.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>go</category>
      <category>fcm</category>
      <category>cobra</category>
    </item>
    <item>
      <title>A2A adventures</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Mon, 03 Nov 2025 17:43:20 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/a2a-adventures-iil</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/a2a-adventures-iil</guid>
      <description>&lt;h2&gt;
  
  
  Building a GitHub Issues Agent with FastAPI and A2A
&lt;/h2&gt;

&lt;p&gt;This project demonstrates a small agent that receives A2A JSON-RPC messages and returns GitHub issues for a repository. It is intentionally minimal — focusing on clear message schemas, simple HTTP fetch logic, and deterministic behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Motivation
&lt;/h3&gt;

&lt;p&gt;I wanted an agent that can be called by other systems via an A2A JSON-RPC contract and that returns a small, well-formed payload containing issue metadata and a short textual summary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI for the HTTP server and lifecycle management&lt;/li&gt;
&lt;li&gt;Pydantic models (in &lt;code&gt;models/a2a.py&lt;/code&gt;) for the message contract&lt;/li&gt;
&lt;li&gt;A single agent implementation (&lt;code&gt;agents/github_issues_agent.py&lt;/code&gt;) that extracts the repository and calls a tool function &lt;code&gt;utils.fetch_issues&lt;/code&gt; to get the data from GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agents/github_issues_agent.py&lt;/code&gt; extracts &lt;code&gt;owner/repo&lt;/code&gt; strings from the message parts. It builds a &lt;code&gt;TaskResult&lt;/code&gt; that includes a textual summary and an artifact containing the full issues payload.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;utils/fetch_issues&lt;/code&gt; uses &lt;code&gt;httpx&lt;/code&gt; and handles rate-limit behavior by allowing a &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check it out at github.com/johnafariogun/github_app&lt;br&gt;
— End&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>python</category>
    </item>
    <item>
      <title>🐱 My HNG 13 Stage 0 Task — Building a Simple Cat Facts API with FastAPI</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Thu, 16 Oct 2025 10:45:06 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/my-hng-13-stage-0-task-building-a-simple-cat-facts-api-with-fastapi-hj7</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/my-hng-13-stage-0-task-building-a-simple-cat-facts-api-with-fastapi-hj7</guid>
      <description>&lt;p&gt;After getting to the penultimate stage in HNG12 internship earlier this year, my perfectionist tendencies cropped up again and this time I want to get to the ultimate stage. First though I have to start from the scratch so stage 0 it is.&lt;/p&gt;

&lt;p&gt;The goal for Stage 0 was straightforward:&lt;br&gt;&lt;br&gt;
👉 Build a small backend application that returns some personal info — and something extra (Cats facts).&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Project Overview
&lt;/h2&gt;

&lt;p&gt;This project, called &lt;strong&gt;Cat Facts API Integration&lt;/strong&gt;, fetches random cat facts from the public &lt;a href="https://catfact.ninja/fact" rel="noopener noreferrer"&gt;Cat Facts API&lt;/a&gt; and combines them with basic user information.&lt;/p&gt;

&lt;p&gt;It includes three simple endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; → Root welcome route
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt; → Health check
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/me&lt;/code&gt; → My personal info + a random cat fact
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All built with &lt;strong&gt;FastAPI&lt;/strong&gt;, &lt;strong&gt;httpx&lt;/strong&gt;, and a bit of structured logging.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
HNG13_simple_rest_stage_0/
├── app.py
├── app.log
├── requirements.txt
└── README.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;The app includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FastAPI&lt;/code&gt; for routing and documentation
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;httpx&lt;/code&gt; for making async API requests
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;logging&lt;/code&gt; for monitoring
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CORS middleware&lt;/code&gt; for flexible frontend integration if need be
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧭 Endpoints Documentation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Root endpoint that returns a welcome message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirms that the API is healthy and reachable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/me&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns user info (email, name, stack) along with a random cat fact.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🧪 Example Response
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GET /me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "status": "success",&lt;br&gt;
  "user": {&lt;br&gt;
    "email": "afariogun.john2002@gmail.com",&lt;br&gt;
    "name": "John Afariogun",&lt;br&gt;
    "stack": "Python/FastAPI"&lt;br&gt;
  },&lt;br&gt;
  "timestamp": "2025-10-15T10:00:00Z",&lt;br&gt;
  "fact": "Cats sleep for around 70% of their lives."&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧰 Running the App Locally
&lt;/h2&gt;

&lt;p&gt;To test this project on your system:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
git clone https://github.com/johnafariogun/HNG13_simple_rest_stage_0&lt;br&gt;
cd HNG13_simple_rest_stage_0&lt;br&gt;
pip install -r requirements.txt&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then start the server:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
python app.py&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Visit these URLs in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://127.0.0.1:8080/me" rel="noopener noreferrer"&gt;http://127.0.0.1:8080/me&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://127.0.0.1:8080/docs" rel="noopener noreferrer"&gt;http://127.0.0.1:8080/docs&lt;/a&gt; → to explore the auto-generated FastAPI documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You can checkout (mine)[&lt;a href="https://hng13simplereststage0-production.up.railway.app/me" rel="noopener noreferrer"&gt;https://hng13simplereststage0-production.up.railway.app/me&lt;/a&gt;] 
&lt;/h2&gt;

&lt;h2&gt;
  
  
  📸 OpenAPI Documentation
&lt;/h2&gt;

&lt;p&gt;FastAPI automatically provides Swagger UI for your routes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧭 Navigate to &lt;code&gt;/docs&lt;/code&gt;&lt;br&gt;
You’ll see your &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/health&lt;/code&gt;, and &lt;code&gt;/me&lt;/code&gt; routes neatly documented — no extra setup needed!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🌍 Deployment
&lt;/h2&gt;

&lt;p&gt;So I deployed this on Railway&lt;/p&gt;

&lt;p&gt;You can check out how to deploy a fastapi app on railway at the &lt;a href="https://docs.railway.com/guides/fastapi" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 What I Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to set up a simple FastAPI app&lt;/li&gt;
&lt;li&gt;How to call external APIs asynchronously using &lt;code&gt;httpx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to implement logging and error handling&lt;/li&gt;
&lt;li&gt;How to structure JSON responses neatly&lt;/li&gt;
&lt;li&gt;The power of automatic documentation in FastAPI&lt;/li&gt;
&lt;li&gt;How to deploy using Railway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔚 Conclusion
&lt;/h2&gt;

&lt;p&gt;Stage 0 may look simple, but it’s where the foundation for cleaner, production-ready APIs begins.&lt;/p&gt;

&lt;p&gt;This project taught me how small things — like handling timeouts, errors, and logs — make a huge difference when building real backend systems.&lt;/p&gt;

&lt;p&gt;Next step? &lt;strong&gt;Stage 1 of HNG 13&lt;/strong&gt;, probably focusing on testing, deployment, and CI/CD 🚀&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👨‍💻 Author:&lt;/strong&gt; John Afariogun&lt;br&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Python / FastAPI&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="//github.com/johnafariogun"&gt;@johnafariogun&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Email:&lt;/strong&gt; &lt;a href="//mailto:afariogun.john2002@gmail.com"&gt;afariogun.john2002@gmail.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

</description>
      <category>backend</category>
      <category>railway</category>
      <category>cats</category>
      <category>python</category>
    </item>
  </channel>
</rss>
