{"id":5307,"date":"2026-08-30T14:30:39","date_gmt":"2026-08-30T05:30:39","guid":{"rendered":"https:\/\/donguri3.net\/server-tech\/oci-nlb-free-failover-2\/"},"modified":"2026-08-30T14:30:40","modified_gmt":"2026-08-30T05:30:40","slug":"oci-nlb-free-failover","status":"publish","type":"post","link":"https:\/\/donguri3.net\/en\/server-tech\/linux-server-network\/oci-nlb-free-failover\/","title":{"rendered":"Zero Additional Cost for Fault Tolerance: Redundant Home Server Entry Point with OCI Network Load Balancer (NLB)"},"content":{"rendered":"<p>Previously, I operated a setup where an instance (OCI1) was deployed on OCI (Oracle Cloud Infrastructure)\u2014utilizing the Always Free tier\u2014as the &#8220;entry point&#8221; to externally publish my home server, forwarding traffic to my home network via a public IP.<\/p>\n<p>However, an issue occurred where OCI1 was running, but network communication completely failed. Checking via console connection revealed that the OS itself was responding, and no software-level failures (such as fatal typos in configuration files or stopped services) could be found. There was also no disruption information reported on the OCI side.<\/p>\n<p>What made it even trickier was that a second instance (OCI2), prepared for testing purposes, was operating normally. The failure was isolated to just one instance, and moreover, several outages lasting a few hours per day occurred repeatedly. Observational data showed that monitoring metrics were failing to collect exclusively for OCI1 during the periods when the failure occurred.<\/p>\n<p>While I intended to continue investigating the root cause, I couldn&#8217;t afford to have my entry point completely shut down, so I decided to pivot toward building structural resilience.<\/p>\n<h2>Goal: Keep the Entry Point Up Without Spending a Dime<\/h2>\n<p>The objective is simple.<\/p>\n<ul>\n<li>Zero additional cost (completely within the Always Free tier)<\/li>\n<li>Automatic failover to OCI2 to maintain connectivity even if OCI1 goes down<\/li>\n<li>No manual IP remapping or DNS switching required<\/li>\n<\/ul>\n<p>Assuming that &#8220;unexplained outages&#8221; would happen again, I first set out to eliminate the single point of failure.<\/p>\n<h2>The Complication: Only OCI1 Couldn&#8217;t Communicate Externally<\/h2>\n<p>For OCI1, which I had been operating, I was able to connect to the console using the Cloud Shell connection located in the OS Management tab of the instance management screen\u2014meaning the OS was alive. On the other hand, metrics were not being collected, and incoming\/outgoing communication was impossible\u2014meaning it was dead as an entry point. Furthermore, OCI2 was completely healthy.<\/p>\n<p><a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-2524\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df.png\" alt=\"Outage Period\" width=\"927\" height=\"438\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df.png 927w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-300x142.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-768x363.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-530x250.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-565x267.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-710x335.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/b4e7adf0be4d5680087a742270dbc5df-725x343.png 725w\" sizes=\"(max-width: 927px) 100vw, 927px\" \/><\/a><\/p>\n<p>I repeatedly tried &#8220;rebooting to see if it fixes it,&#8221; but nothing changed. Therefore, I decided to build a &#8220;failure-resilient mechanism&#8221; before investigating the root cause.<\/p>\n<h2>Achieving Redundancy for $0: Using a Network Load Balancer (NLB) as the Entry Point<\/h2>\n<p>This is where I introduced the Network Load Balancer, commonly referred to as NLB. Instead of an L7 load balancer that routes traffic based on HTTP hostnames, think of it as an &#8220;entry point&#8221; that primarily handles TCP\/UDP traffic.<\/p>\n<p>An important aspect is its compatibility with the &#8220;free tier constraint&#8221;: OCI documentation explicitly states that <a href=\"https:\/\/docs.oracle.com\/en-us\/iaas\/Content\/NetworkLoadBalancer\/overview.htm?utm_source=chatgpt.com#lb-vs-nlb\">Network Load Balancers are included in the Always Free tier<\/a> (at the time of writing).<\/p>\n<p>Additionally, regarding networking costs, official pricing pages state that inbound data is free, and <a href=\"https:\/\/www.oracle.com\/cloud\/networking\/pricing\/\">outbound data is &#8220;free for the first 10 TB&#8221;<\/a> (though be mindful of limits and specific conditions).<\/p>\n<p>Here is what I did:<\/p>\n<ul>\n<li>Installed WireGuard and nginx on OCI2 as well, making it an equivalent entry point to OCI1<\/li>\n<li>Created an NLB and registered OCI1\/OCI2 as backends<\/li>\n<li>Unified the external entry point flow to: &#8220;NLB &rarr; (OCI1 or OCI2) &rarr; Home Server&#8221;<\/li>\n<\/ul>\n<p>With this setup, if OCI1 becomes unreachable, its backend health check will fail, removing it from the rotation and automatically steering traffic toward OCI2.<\/p>\n<p><a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e.png\"><img decoding=\"async\" class=\"aligncenter size-large wp-image-2523\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-1024x576.png\" alt=\"Unified external entry point flow to: NLB -&gt; (OCI1 or OCI2) -&gt; Home Server\" width=\"1024\" height=\"576\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-1024x576.png 1024w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-300x169.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-768x432.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-1536x864.png 1536w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-320x180.png 320w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-530x298.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-565x318.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-710x399.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e-725x408.png 725w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/12\/cc0e69512dfcebc96e4cb35d580d4d6e.png 1920w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/p>\n<h2>Configuration Steps: Creating an NLB (Console)<\/h2>\n<p>Here is where you can find the menu:<\/p>\n<p>Networking &gt; Network Load Balancers &gt; Create Network Load Balancer<\/p>\n<p>From here on, specific names have been replaced with placeholders for the article (e.g., vcn-main, subnet-nlb, instance-a, etc.). Please substitute them with your actual environment names.<\/p>\n<h3>Add Details<\/h3>\n<ul>\n<li>Network load balancer name: nlb-home-entry<\/li>\n<li>Compartment: my-compartment<\/li>\n<li>Visibility type: Public (if you want to receive traffic from the internet)<\/li>\n<li>Public IP address assignment: Ephemeral IPv4<br \/>\n(Reserved IPv4 is also fine if you want a static IP)<\/li>\n<li>VCN: vcn-main<\/li>\n<li>Subnet: subnet-nlb<\/li>\n<li>NSG: Unspecified for now (can be attached later)<\/li>\n<\/ul>\n<p>If your goal is to &#8220;get things running under free-tier constraints first,&#8221; this simple configuration is more than enough.<\/p>\n<h3>Configure Listener<\/h3>\n<p>Since my use case was to &#8220;handle multiple ports and protocols like WireGuard and port forwarding collectively at the entry point,&#8221; I configured it to handle UDP and TCP together, listening on &#8220;Any port.&#8221;<\/p>\n<ul>\n<li>Listener name: listener-any<\/li>\n<li>Traffic type: UDP\/TCP<\/li>\n<li>Ingress traffic port: Use any port<\/li>\n<\/ul>\n<p>Choosing &#8220;Any port&#8221; seems to be treated as port 0 \/ wildcard.<\/p>\n<ul>\n<li>You can temporarily set the timeout to match screen constraints (e.g., 120 seconds). Get it working first, then fine-tune later.<\/li>\n<\/ul>\n<h3>Select Backends (Backend Set)<\/h3>\n<ul>\n<li>Backend set name: backendset-home<\/li>\n<li>Add backends: Add two compute instances\n<ul>\n<li>instance-a (e.g., 10.0.0.10)<\/li>\n<li>instance-b (e.g., 10.0.0.11)<\/li>\n<\/ul>\n<\/li>\n<li>Port: Any<\/li>\n<li>Load balancing policy: 5-tuple hash<\/li>\n<\/ul>\n<p>For the load balancing policy, I started with the default 5-tuple hash (when in doubt, default is fine).<\/p>\n<h3>Security Lists (Important)<\/h3>\n<p>As the warning on the screen says: &#8220;Make sure to manually configure security list rules after creation,&#8221; skipping this step will cause health checks to fail, resulting in a Critical status.<\/p>\n<ul>\n<li>You can choose &#8220;Automatically add&#8221; or, if it conflicts with your operational policy, go with &#8220;Manual.&#8221;<\/li>\n<li>If manual, you must at least allow:\n<ul>\n<li>NLB &rarr; Backend (e.g., required ports such as 80\/udp,tcp)<\/li>\n<li>Health check reachability (refer to &#8220;Health check source IP&#8221; mentioned below)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3>Health Check Policy<\/h3>\n<p>This was the tricky part I stumbled on.<\/p>\n<ul>\n<li>Protocol: HTTP<\/li>\n<li>Port: 80<\/li>\n<li>Interval: 10000ms<\/li>\n<li>Timeout: 3000ms<\/li>\n<li>Retries: 3<\/li>\n<li>URL path: \/<\/li>\n<li>Status code (expected value): 301 (change according to your environment; I initially set it to 200)<\/li>\n<\/ul>\n<p>If left as is, when your nginx configuration redirects all HTTP traffic to HTTPS with a 301 status, it will conflict with the health check&#8217;s expected value.<\/p>\n<h2>The Stumbling Block: Backend Health is Critical and Traffic Won&#8217;t Forward<\/h2>\n<p>Even if you create an NLB, traffic naturally won&#8217;t forward if the backend health is Critical. In my case, the return value of the health check was the culprit.<\/p>\n<p>My nginx configuration was set up to 301 redirect all HTTP (80) traffic to HTTPS.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">server {\n&amp;nbsp; &amp;nbsp; listen 80;\n&amp;nbsp; &amp;nbsp; listen &#x5B;::]:80;\n&amp;nbsp; &amp;nbsp; server_name donguri3.net www.donguri3.net;\n\n&amp;nbsp; &amp;nbsp; return 301 https:\/\/$host$request_uri;\n}<\/pre>\n<p>&nbsp;<\/p>\n<p>If you run `curl -I` against the private IP in this state, it will naturally return `HTTP\/1.1 301`. In other words, nginx is returning a 301 &#8220;correctly.&#8221;<br \/>\nHowever, if the NLB is configured to &#8220;expect an HTTP 200,&#8221; it treats the 301 response as unhealthy, resulting in a Critical status.<\/p>\n<p>Once resolved, the overall health status turns to OK, and the Network Load Balancer starts functioning properly.<\/p>\n<h2>Conclusion: Make Your Entry Point Resilient for $0, Even Without Knowing the Root Cause<\/h2>\n<p>When OCI1 became &#8220;unreachable despite being running,&#8221; checking the console yielded no clues. There was no OCI outage notice. OCI2 was operating normally. Furthermore, metrics were missing only on OCI1\u2014under these conditions, fighting the issue by purely investigating the cause would mean prolonged downtime at the entry point.<\/p>\n<p>Therefore, I prepared an entry point (WireGuard + nginx) on OCI2 as well, created a Network Load Balancer (NLB) within the Always Free tier limits, and made the entry point redundant.<\/p>\n<p>By matching the health check&#8217;s expected status code to 301 to accommodate nginx&#8217;s behavior, I achieved an &#8220;uninterrupted entry point&#8221; with zero additional cost. While root cause analysis can be done later, the stress of a dead entry point can be eliminated first.<\/p>\n<p>If you run into the same issue, definitely give this a try.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Previously, I operated a setup where an instance (OCI1) was deployed on OCI (Oracle Cloud Infrastructure)\u2014util [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":2523,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_locale":"en_US","_original_post":"https:\/\/donguri3.net\/?p=2522","footnotes":""},"categories":[1170],"tags":[52,63,20,21,92,10,922,131],"class_list":["post-5307","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux-server-network","tag-https","tag-nginx","tag-oci","tag-oracle-cloud-infrastructure","tag-wireguard","tag-server","tag-922","tag-131","en-US"],"_links":{"self":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5307","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/comments?post=5307"}],"version-history":[{"count":1,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5307\/revisions"}],"predecessor-version":[{"id":5310,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5307\/revisions\/5310"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media\/2523"}],"wp:attachment":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media?parent=5307"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/categories?post=5307"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/tags?post=5307"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}