<?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.us-east-2.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 Physics of AI on Your Own Machine: Inference Engineering, Local-First</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:24:40 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/the-hidden-physics-of-ai-on-your-own-machine-inference-engineering-local-first-4f9p</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/the-hidden-physics-of-ai-on-your-own-machine-inference-engineering-local-first-4f9p</guid>
      <description>&lt;p&gt;You close your laptop lid on a plane, open a chat window with no wifi bar in sight, and type a question anyway. A moment later, text starts appearing, not from a server farm somewhere over the ocean, but from the silicon six inches from your fingers. No round trip. No data leaving the device. Just your machine, quietly doing the work.&lt;/p&gt;

&lt;p&gt;That experience is the promise of local-first AI. And getting there is not a matter of shrinking a cloud service down to fit, it's a completely different physical problem, governed by the same brutal constraints as data-center inference, but with almost none of the data center's luxuries.&lt;/p&gt;

&lt;p&gt;For years, the AI industry's center of gravity was training: enormous clusters, oceans of data, model weights forged in facilities the size of small towns. Running the model afterward was treated as an afterthought, something a server rack in the cloud would handle. But a parallel movement has been building quietly, engineers determined to make inference happen entirely on the device in your hand, your bag, or your desk. This is inference engineering turned inward, and it forces you to relearn the discipline under a much stricter budget.&lt;/p&gt;

&lt;p&gt;If cloud inference is a symphony performed by a full orchestra in a concert hall with unlimited musicians on call, local-first inference is a single performer playing every part themselves, on an instrument they carry with them, with no stagehands and no backup if a string breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Machine, No Safety Net
&lt;/h2&gt;

&lt;p&gt;Explore local-first inference and you quickly notice that the tidy three-layer picture of cloud AI – runtime, infrastructure, and tooling – collapses into something much smaller and much more personal.&lt;/p&gt;

&lt;p&gt;The Runtime is still there, and it still matters enormously: the code that meets the model where it touches the chip, whether that chip is an Apple silicon GPU, a Snapdragon NPU, or the integrated graphics inside a budget laptop. But the second layer, Infrastructure, effectively disappears. There is no cluster to scale horizontally, no load balancer routing traffic across regions, no fleet of machines standing by. There is exactly one device, with a fixed, unchangeable amount of memory and compute, and it must handle everything itself. If it runs out of headroom, there is no second server to fail over to the request simply doesn't complete.&lt;/p&gt;

&lt;p&gt;What remains of the Tooling layer shrinks too, from dashboards monitoring thousands of requests per second down to a single process on a single machine but its job becomes more intimate: figuring out, in real time, exactly how much of this one device's limited resources it's safe to use before everything else the person is doing grinds to a halt.&lt;/p&gt;

&lt;p&gt;This is the defining shift of local-first engineering: you're no longer designing for elastic scale. You're designing for a hard, physical ceiling that cannot be raised by throwing more hardware at the problem, because the hardware is whatever the person already owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economy of a Shared Device
&lt;/h2&gt;

&lt;p&gt;Cloud inference engineers argue over milliseconds of Time to First Token or fractions of a cent per million tokens. Local-first engineers inherit those same trade-offs, but with a new constraint layered on top of all of them: the device isn't dedicated to the model. It's also running someone's browser tabs, their video call, their music.&lt;/p&gt;

&lt;p&gt;This changes what "the right metric" even means. A cloud system can be tuned purely for throughput or purely for latency because it exists to do one job. A phone or laptop has to share. So local-first inference engineering adds a metric that barely exists in the data center: resource courtesy how much battery, memory, and thermal headroom the model is allowed to consume before it starts degrading everything else the person is trying to do. Push too hard for speed and the fan spins up, the battery drains in an hour, and the rest of the operating system starts stuttering. Optimize too conservatively and the response takes so long the feature feels unusable.&lt;/p&gt;

&lt;p&gt;Getting this budget wrong is just as costly locally as it is in the cloud, arguably more so, because there's no ops team watching a dashboard to catch it. If a model is quantized down aggressively to fit in the memory a phone can spare, and that compression quietly damages its ability to reason or write code, there's no server-side rollback. The degraded model is just what the person has, until they close the app or update it. Evaluation has to happen before the model ever ships to the device, because once it's local, it's largely on its own.&lt;/p&gt;

&lt;p&gt;What makes local-first inference worth the trouble, despite these tighter constraints, is what it buys back: no network dependency, no data ever leaving the machine, no bill that scales with usage, and a response that keeps coming even at thirty thousand feet with no signal at all. The physics is harder. The trade-off, for a growing number of engineers and users, is worth it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hardware</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Powering Local-First AI: Searching and Retrieving Context for Inference</title>
      <dc:creator>John Afariogun</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:06:04 +0000</pubDate>
      <link>https://dev.to/john_afariogun_e2351c78af/powering-local-first-ai-searching-and-retrieving-context-for-inference-3ohl</link>
      <guid>https://dev.to/john_afariogun_e2351c78af/powering-local-first-ai-searching-and-retrieving-context-for-inference-3ohl</guid>
      <description>&lt;p&gt;This week, I tackled the Search and Retrieval portion of our Local Context Store &lt;a href="https://github.com/johnafariogun/local-first-AI/tree/feature/m01w02-search-context" rel="noopener noreferrer"&gt;Github link&lt;/a&gt;. If capturing data cleanly is about keeping our memory system from turning into a messy Downloads folder, searching is about giving the AI the exact file it needs the moment it asks. My task was to build the retrieval layer, the critical bridge between our stored SQLite records and the prompt window of a Large Language Model (LLM).&lt;/p&gt;

&lt;p&gt;The goal was simple but vital: when a user asks the AI a question, the system needs to seamlessly dive into its local memory, find the most relevant past context, and package it up so the LLM can reason with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the Right Needles
&lt;/h3&gt;

&lt;p&gt;To make this happen, I implemented a suite of search functions, such as &lt;code&gt;search_context_items&lt;/code&gt; and &lt;code&gt;search_context_by_type&lt;/code&gt;. Under the hood, these use SQLite's &lt;code&gt;LIKE&lt;/code&gt; operator to perform case-insensitive keyword searches across both the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; of our stored memory. This ensures that if the system is looking for "sqlite", it will reliably find records mentioning "SQLite" regardless of how it was capitalized, and whether the keyword is hiding in the headline or deep in the body text.&lt;/p&gt;

&lt;p&gt;I also added the ability to filter these searches by strict context types. If the AI is trying to remember a technical choice, we can restrict the search solely to &lt;code&gt;config_decision&lt;/code&gt; records, entirely ignoring irrelevant &lt;code&gt;device_log&lt;/code&gt; or &lt;code&gt;learning_record&lt;/code&gt; entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ranking for the Context Window
&lt;/h3&gt;

&lt;p&gt;Finding matching text is only half the battle. Because an LLM's context window is limited and valuable, we can't just feed the model a random assortment of keyword matches. We have to guarantee that the &lt;em&gt;best&lt;/em&gt; information surfaces first.&lt;/p&gt;

&lt;p&gt;I solved this by enforcing a strict hierarchy in the database queries: &lt;code&gt;ORDER BY importance DESC, created_at DESC&lt;/code&gt;. This means high-priority memories are always fetched before trivial notes, and recent memories are prioritized over older ones. Furthermore, I pushed the &lt;code&gt;LIMIT&lt;/code&gt; constraint directly down to the database level. Instead of fetching hundreds of rows into Python memory just to slice off the top five, the database does the heavy lifting, returning only the most critical handful of records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Translating Data for the LLM
&lt;/h3&gt;

&lt;p&gt;Once the database returns those top records, they are still just raw data dictionaries. To be useful, they have to be translated into a language the LLM understands natively: clean, structured text.&lt;/p&gt;

&lt;p&gt;I built the &lt;code&gt;prepare_context_for_inference&lt;/code&gt; function to transform those raw rows into a highly readable text block. Instead of dumping raw JSON into the prompt, the AI receives cleanly formatted blocks indicating the index, type, title, and content of each memory. This structural clarity helps the LLM easily parse the context boundaries so it doesn't confuse a past project note with the user's current question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proving Local Speed
&lt;/h3&gt;

&lt;p&gt;Finally, because this is a &lt;em&gt;local-first&lt;/em&gt; system, speed is non-negotiable. I integrated a lightweight timing mechanism to measure exactly how fast these database lookups happen. Seeing the context retrieved and formatted in just a few milliseconds proved that our local SQLite approach is blazing fast, meaning we can inject memories into prompts without introducing noticeable lag to the user experience.&lt;/p&gt;

&lt;p&gt;This task proved to me that effective AI memory isn't just about storing data. It relies just as heavily on efficiently ranking, filtering, and structuring that data for inference. With this retrieval layer in place, our AI won't just passively store memories, it will actively know how to use them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <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>
