<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title><![CDATA[Sage Ideas — Cloud & Infrastructure]]></title>
    <description><![CDATA[Deep dives on AWS, Supabase, Terraform, Docker, monitoring, CI/CD, and production infrastructure patterns.]]></description>
    <link>https://www.sageideas.dev/topics/cloud-infra</link>
    <atom:link href="https://www.sageideas.dev/feed/cloud-infra.xml" rel="self" type="application/rss+xml"/>
    <language>en-us</language>
    <managingEditor>sage@sageideas.dev (Jason Teixeira)</managingEditor>
    <lastBuildDate>Wed, 05 Aug 2026 06:02:27 GMT</lastBuildDate>
    <ttl>60</ttl>
    <item>
      <title><![CDATA[Terraform Module Patterns: How I Structure IaC for Reuse]]></title>
      <description><![CDATA[Opinionated Terraform module patterns — consistent variable naming, output contracts, testing with Terratest, and the module structure that works across teams.]]></description>
      <content:encoded><![CDATA[<h1>Terraform Module Patterns: How I Structure IaC for Reuse</h1>
<p>After building the AWS Landing Zone and multiple infrastructure projects, I&#39;ve developed opinions about how to write Terraform modules that other people can actually use.</p>
<h2>The Module Structure</h2>
<p>Every module follows this structure:</p>
<p>\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/terraform-module-patterns-how-i-structure-iac-for-reuse</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/terraform-module-patterns-how-i-structure-iac-for-reuse</guid>
      <pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate>
      <category><![CDATA[Cloud Automation]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Docker in CI/CD: The Patterns That Cut My Pipeline Time by 82%]]></title>
      <description><![CDATA[Layer caching, multi-stage builds, BuildKit, and the Docker patterns that took my CI pipeline from 45 minutes to 8 minutes.]]></description>
      <content:encoded><![CDATA[<h1>Docker in CI/CD: The Patterns That Cut My Pipeline Time by 82%</h1>
<p>My CI pipeline used to take 45 minutes. It now takes 8. The biggest wins came from Docker optimization — not faster hardware.</p>
<h2>The Problem</h2>
<p>Every CI run was:</p>
<ol>
<li>Pull base image (2 min)</li>
<li>Install OS dependencies (5 min)</li>
<li>Install Python packages (8 min)</li>
<li>Install Node packages (6 min)</li>
<li>Build application (4 min)</li>
<li>Run tests (15 min)</li>
<li>Build production image (5 min)</li>
</ol>
<p>Total: ~45 minutes. Developers stopped running the full pipeline. Bugs slipped through.</p>
<h2>Fix 1: Multi-Stage Builds (45 → 30 min)</h2>
<p>\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/docker-in-ci-cd-the-patterns-that-cut-my-pipeline-time-by-82</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/docker-in-ci-cd-the-patterns-that-cut-my-pipeline-time-by-82</guid>
      <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
      <category><![CDATA[DevOps]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[AWS Cost Optimization: How I Keep a Production Platform Under $50/Month]]></title>
      <description><![CDATA[The Nexural platform runs on AWS with Vercel, Supabase, and targeted AWS services. Here's how I keep costs under $50/month for a platform with 185 tables and real-time data.]]></description>
      <content:encoded><![CDATA[<h1>AWS Cost Optimization: How I Keep a Production Platform Under $50/Month</h1>
<p>The Nexural ecosystem has 185 database tables, 69 API endpoints, real-time market data, AI-powered features, and a live quality dashboard. My AWS bill is under $50/month.</p>
<p>Here&#39;s how.</p>
<h2>The Architecture That Saves Money</h2>
<p><strong>Principle: use managed services at their free/cheap tiers instead of running your own infrastructure.</strong></p>
<table>
<thead>
<tr>
<th>Service</th>
<th>What It Does</th>
<th>Monthly Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Vercel (Hobby → Pro)</td>
<td>Next.js hosting, edge functions</td>
<td>$0-20</td>
</tr>
<tr>
<td>Supabase (Free → Pro)</td>
<td>PostgreSQL, Auth, Real-time</td>
<td>$0-25</td>
</tr>
<tr>
<td>AWS S3</td>
<td>Telemetry data, artifacts</td>
<td>$0.02</td>
</tr>
<tr>
<td>AWS Lambda</td>
<td>API proxy, telemetry ingestion</td>
<td>$0 (free tier)</td>
</tr>
<tr>
<td>AWS API Gateway</td>
<td>Lambda HTTP endpoint</td>
<td>$0 (free tier)</td>
</tr>
<tr>
<td>AWS CloudFront</td>
<td>CDN + WAF</td>
<td>$0 (free tier)</td>
</tr>
<tr>
<td>GitHub Actions</td>
<td>CI/CD, scheduled jobs</td>
<td>$0 (free for public repos)</td>
</tr>
</tbody></table>
<p><strong>Total: ~$25-45/month</strong> for a production platform.</p>
<h2>The Tricks</h2>
<h3>1. Supabase Instead of RDS</h3>
<p>A Supabase Pro instance is $25/month and includes:</p>
<ul>
<li>PostgreSQL 15 with 8GB storage</li>
<li>Row-level security</li>
<li>Real-time subscriptions</li>
<li>Built-in authentication</li>
<li>Auto-backups</li>
</ul>
<p>An equivalent RDS instance (db.t3.micro) is $15/month but you need to manage backups, auth, and real-time yourself. Add those services and you&#39;re at $60+.</p>
<h3>2. Lambda for Spiky Workloads</h3>
<p>The telemetry ingestion API handles 0 requests most of the time, then bursts during CI runs. Lambda is perfect: $0 when idle, pennies during bursts.</p>
<p>\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/aws-cost-optimization-how-i-keep-a-production-platform-under-50-month</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/aws-cost-optimization-how-i-keep-a-production-platform-under-50-month</guid>
      <pubDate>Sun, 08 Mar 2026 00:00:00 GMT</pubDate>
      <category><![CDATA[Cloud Automation]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[GitHub OIDC → AWS (No Long-Lived Keys): Cloud Automation the Right Way]]></title>
      <description><![CDATA[How to use GitHub Actions OIDC to assume an AWS IAM role and deploy/upload artifacts without storing AWS keys. Includes least-privilege IAM, trust policy patterns, and troubleshooting tips.]]></description>
      <content:encoded><![CDATA[<h1>GitHub OIDC → AWS (No Long-Lived Keys): Cloud Automation the Right Way</h1>
<p>Static AWS keys in CI are a footgun.</p>
<p>If you want cloud automation that scales (and passes security review), use <strong>OIDC-based federation</strong>:</p>
<ul>
<li>GitHub Actions issues a short-lived identity token (OIDC)</li>
<li>AWS STS exchanges it for short-lived AWS credentials</li>
<li>Your workflow assumes a least-privilege role and does the work</li>
</ul>
<p>This portfolio uses the same pattern to support <strong>Cloud telemetry mode</strong> (AWS S3) without ever embedding long-lived credentials.</p>
<h2>The architecture</h2>
<p>\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/github-oidc-aws-no-long-lived-keys-cloud-automation-the-right-way</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/github-oidc-aws-no-long-lived-keys-cloud-automation-the-right-way</guid>
      <pubDate>Sat, 10 Jan 2026 00:00:00 GMT</pubDate>
      <category><![CDATA[Cloud Automation]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Authentication Is Harder Than You Think]]></title>
      <description><![CDATA[I've implemented auth 4 times across different projects. Every time I thought it would take 2 days. Every time it took 2 weeks. Here's why, and what I'd do differently.]]></description>
      <content:encoded><![CDATA[<h1>Authentication Is Harder Than You Think</h1>
<p>Every project plan I&#39;ve ever written has a line item: &quot;Authentication — 2 days.&quot;</p>
<p>Every project retrospective has a note: &quot;Auth took 2 weeks.&quot;</p>
<p>I&#39;ve built auth systems 4 times now. Each time, I underestimate it. Here&#39;s why, and what I finally learned.</p>
<h2>The Iceberg</h2>
<p>What you think auth is:</p>
<ul>
<li>Login form</li>
<li>Store a token</li>
<li>Check if token is valid</li>
<li>Done</li>
</ul>
<p>What auth actually is:</p>
<ul>
<li>Login form (email/password + OAuth + magic links + MFA?)</li>
<li>Password hashing (bcrypt, argon2, what cost factor?)</li>
<li>Session management (JWT vs session cookie vs both?)</li>
<li>Token refresh (silent refresh, rotation, revocation)</li>
<li>CSRF protection (same-site cookies, double-submit token)</li>
<li>Rate limiting (on login, on registration, on password reset)</li>
<li>Password reset flow (token generation, expiry, single-use)</li>
<li>Email verification (token, resend logic, what if they change email?)</li>
<li>Account lockout (how many attempts? What&#39;s the unlock flow?)</li>
<li>Role-based access (admin vs user vs moderator)</li>
<li>API key management (for programmatic access)</li>
<li>Session invalidation on password change</li>
<li>&quot;Remember me&quot; vs &quot;this session only&quot;</li>
<li>Login from new device notification</li>
<li>Audit logging (who logged in, when, from where)</li>
</ul>
<p>That&#39;s 15+ features. At 1-2 days each, you&#39;re looking at a month.</p>
<h2>What I Do Now: Use Supabase Auth and Extend</h2>
<p>After building custom auth twice and hating my life both times, I now start with Supabase Auth (or Clerk, or Auth.js). It handles:</p>
<ul>
<li>Email/password with bcrypt</li>
<li>OAuth providers (Google, GitHub, Discord)</li>
<li>JWT tokens with refresh</li>
<li>Email verification</li>
<li>Password reset</li>
<li>Session management</li>
<li>Rate limiting</li>
</ul>
<p>That&#39;s 80% of auth, handled by people who think about auth full-time. I focus on the 20% that&#39;s specific to my app:</p>
<p>\\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/authentication-is-harder-than-you-think</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/authentication-is-harder-than-you-think</guid>
      <pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate>
      <category><![CDATA[Architecture]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Rate Limiting: The Feature Nobody Thinks About Until It's Too Late]]></title>
      <description><![CDATA[Your API works perfectly at 10 requests per second. At 10,000, it falls over. Here's how I implement rate limiting that protects without annoying legitimate users.]]></description>
      <content:encoded><![CDATA[<h1>Rate Limiting: The Feature Nobody Thinks About Until It&#39;s Too Late</h1>
<p>Nobody puts &quot;implement rate limiting&quot; on the sprint board. It&#39;s not a user story. It doesn&#39;t move a metric. Product never asks for it.</p>
<p>Then one day, someone scripts 50,000 requests to your API in 30 seconds and your database melts. Or worse — a single user&#39;s runaway script costs you $800 in AWS Lambda invocations overnight.</p>
<p>Both of these happened to me. Now rate limiting is in my starter template.</p>
<h2>The Three Layers</h2>
<p>I implement rate limiting at three layers, because each catches different abuse patterns:</p>
<h3>Layer 1: Edge (CloudFront / Vercel)</h3>
<p>\\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/rate-limiting-the-feature-nobody-thinks-about-until-it</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/rate-limiting-the-feature-nobody-thinks-about-until-it</guid>
      <pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate>
      <category><![CDATA[Architecture]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Supabase in Production: What I Wish I Knew Before 185 Tables]]></title>
      <description><![CDATA[After a year of running Supabase in production with 185 tables, here's the honest review — what's incredible, what's frustrating, and what almost made me switch.]]></description>
      <content:encoded><![CDATA[<h1>Supabase in Production: What I Wish I Knew Before 185 Tables</h1>
<p>I&#39;ve been running Supabase in production for over a year. 185 tables. 69 API endpoints. Stripe webhooks. Real-time subscriptions. Discord bot data. Trading analytics.</p>
<p>This isn&#39;t a &quot;getting started&quot; tutorial. This is the honest review after living with it at scale.</p>
<h2>What&#39;s Genuinely Incredible</h2>
<h3>Row-Level Security Changes Everything</h3>
<p>RLS is Supabase&#39;s killer feature, and most people underuse it. Instead of writing authorization checks in every API endpoint, the database enforces access:</p>
<p>\\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/supabase-in-production-what-i-wish-i-knew-before-185-tables</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/supabase-in-production-what-i-wish-i-knew-before-185-tables</guid>
      <pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate>
      <category><![CDATA[Architecture]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Environment Variables: The Security Hole in Every Startup]]></title>
      <description><![CDATA[Your .env file has your database password, Stripe secret key, and AWS credentials. It's in a Slack message, a developer's laptop, and probably a Docker image somewhere. Let's fix that.]]></description>
      <content:encoded><![CDATA[<h1>Environment Variables: The Security Hole in Every Startup</h1>
<p>Quick audit: where is your database password right now?</p>
<p>If you answered &quot;.env file in the repo root&quot; — you&#39;re in the majority. If you answered &quot;also in a Slack message to the new hire, a screenshot in Confluence, and hardcoded in that one Lambda function that Dave wrote before he left&quot; — you&#39;re being honest.</p>
<p>Environment variables are the most dangerous infrastructure in most startups because everyone treats them as an afterthought.</p>
<h2>The Common Mistakes</h2>
<h3>Mistake 1: .env in Version Control</h3>
<p>I&#39;ve seen it in production repos at real companies. A \\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/environment-variables-the-security-hole-in-every-startup</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/environment-variables-the-security-hole-in-every-startup</guid>
      <pubDate>Sat, 15 Nov 2025 00:00:00 GMT</pubDate>
      <category><![CDATA[Security]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Monitoring That Actually Tells You Something]]></title>
      <description><![CDATA[Dashboards with 47 panels where everything is green aren't monitoring. They're decoration. Here's what I actually monitor and why most alerting is useless noise.]]></description>
      <content:encoded><![CDATA[<h1>Monitoring That Actually Tells You Something</h1>
<p>I once inherited a Grafana instance with 47 dashboard panels. CPU utilization, memory usage, disk I/O, network bytes, JVM heap — every metric you could imagine. Everything was green. All the time.</p>
<p>Two days later, the API went down for 4 hours. Not a single alert fired.</p>
<p>Why? Because CPU was at 22%, memory at 45%, and disk at 30%. All &quot;healthy.&quot; The actual problem was a connection pool exhaustion — a metric nobody was watching.</p>
<h2>The Four Golden Signals (and Nothing Else)</h2>
<p>Google&#39;s SRE book nailed this. You need exactly four signals:</p>
<p><strong>1. Latency</strong> — How long do requests take?
Not average latency — that hides problems. Track P50, P95, and P99:</p>
<ul>
<li>P50 = 200ms means half your users get responses in 200ms (good)</li>
<li>P95 = 800ms means 1 in 20 users waits 800ms (acceptable)</li>
<li>P99 = 5000ms means 1 in 100 users waits 5 seconds (problem)</li>
</ul>
<p>Your P99 is your real performance. The average lies.</p>
<p><strong>2. Traffic</strong> — How many requests are you handling?
This is your baseline. If traffic drops 80% at 2pm on a Tuesday, something is wrong even if all other metrics are green.</p>
<p><strong>3. Errors</strong> — What percentage of requests fail?
Track error rate, not error count. 100 errors out of 1 million requests (0.01%) is fine. 100 errors out of 200 requests (50%) is an outage.</p>
<p><strong>4. Saturation</strong> — How full is your system?
Database connections, memory, queue depth, thread pools. When any resource hits 80% utilization, you need to act — not because it&#39;s broken, but because you&#39;ve lost your headroom.</p>
<h2>My Actual Monitoring Setup</h2>
<p>For the Nexural platform:</p>
<p>\\</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/monitoring-that-actually-tells-you-something</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/monitoring-that-actually-tells-you-something</guid>
      <pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate>
      <category><![CDATA[DevOps]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
    <item>
      <title><![CDATA[Fixing Docker Compose Connection Errors in CI/CD]]></title>
      <description><![CDATA[Spent 4 hours debugging 'Connection refused' errors in Jenkins. Here's what I learned about Docker networking in CI pipelines.]]></description>
      <content:encoded><![CDATA[<h1>Fixing Docker Compose Connection Errors in CI/CD</h1>
<p>Picture this: your Docker Compose setup works perfectly on your local machine. You push to CI, and suddenly every integration test fails with <code>Connection refused</code>.</p>
<p>The database container is &quot;running.&quot; The API container is &quot;healthy.&quot; The test process starts. Then it cannot connect to the service it needs.</p>
<p>This failure looks random until you remember one thing: local Docker networking and CI Docker networking are not the same environment.</p>
<p>:::proof-note title=&quot;The actual lesson&quot; label=&quot;ci note&quot;
Most Docker Compose connection errors in CI are not Docker problems. They are timing, hostname, port, or network-boundary problems that local development hides.
:::</p>
<h2>The local setup lies to you</h2>
<p>On your machine, you might connect to Postgres at <code>localhost:5432</code>.</p>
<p>Inside a Compose network, another container should usually connect to <code>postgres:5432</code>, where <code>postgres</code> is the service name.</p>
<p>In CI, the test runner may be:</p>
<ul>
<li>inside the Compose network</li>
<li>outside the Compose network on the host</li>
<li>inside a CI service container</li>
<li>inside a nested Docker executor</li>
</ul>
<p>Those four cases use different hostnames.</p>
<p>That is why a connection string can be &quot;correct&quot; locally and wrong in the pipeline.</p>
<h2>First, identify where the test process runs</h2>
<p>Before changing ports, ask one question:</p>
<blockquote>
<p>Is the test command running inside a Compose service or on the CI host?</p>
</blockquote>
<p>If tests run inside Compose:</p>
<pre><code class="language-txt">DATABASE_URL=postgres://user:pass@postgres:5432/app
</code></pre>
<p>If tests run on the CI host and Compose published the port:</p>
<pre><code class="language-txt">DATABASE_URL=postgres://user:pass@127.0.0.1:5432/app
</code></pre>
<p>If tests run in a separate CI container, neither may work until the CI platform&#39;s service networking is configured.</p>
<p>:::system-diagram title=&quot;CI networking decision&quot; label=&quot;compose -&gt; tests&quot; nodes=&quot;Compose service,Network,Test runner,Database&quot;
The right hostname depends on where the test runner lives. Service names work inside the Compose network. Published localhost ports work from the host.
:::</p>
<h2>Do not trust <code>depends_on</code> as readiness</h2>
<p><code>depends_on</code> can control start order. It does not guarantee that Postgres, Redis, or your app is ready to accept connections.</p>
<p>The common bad version:</p>
<pre><code class="language-yaml">services:
  api:
    depends_on:
      - postgres
</code></pre>
<p>That only means the <code>postgres</code> container starts before <code>api</code>. It does not mean migrations ran. It does not mean TCP is ready. It does not mean the database accepted authentication.</p>
<p>Use health checks or an explicit wait script.</p>
<pre><code class="language-yaml">services:
  postgres:
    image: postgres:16
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U app&quot;]
      interval: 5s
      timeout: 5s
      retries: 12

  api:
    depends_on:
      postgres:
        condition: service_healthy
</code></pre>
<p>That still does not solve every CI platform, but it removes the most common race.</p>
<h2>Check the four failure classes</h2>
<p>When I see <code>Connection refused</code>, I work through this order.</p>
<p>:::checklist title=&quot;Docker Compose CI checklist&quot; label=&quot;debug order&quot;</p>
<ul>
<li>Confirm the test runner location: host, Compose service, or CI service container</li>
<li>Confirm the hostname: service name vs 127.0.0.1 vs platform service alias</li>
<li>Confirm the published port is actually mapped</li>
<li>Confirm the dependency is healthy before tests start</li>
<li>Print the resolved environment variables in CI without leaking secrets</li>
<li>Run a tiny TCP check before the full test suite
:::</li>
</ul>
<p>The TCP check is boring but useful:</p>
<pre><code class="language-bash">node -e &quot;require(&#39;net&#39;).connect(5432, process.env.DB_HOST).on(&#39;connect&#39;, () =&gt; { console.log(&#39;ok&#39;); process.exit(0) }).on(&#39;error&#39;, e =&gt; { console.error(e.message); process.exit(1) })&quot;
</code></pre>
<p>If that fails, your application test suite is not the thing to debug yet.</p>
<h2>Use different connection strings for different boundaries</h2>
<p>One clean pattern is to make the boundary explicit:</p>
<pre><code class="language-env">DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/app
</code></pre>
<p>Then your CI job chooses the right one based on where the command runs.</p>
<p>This is less magical than trying to make one URL work everywhere.</p>
<p>:::scorecard title=&quot;Connection-string sanity check&quot; label=&quot;scorecard&quot;
Runner location | Hostname | Port source
Inside Compose | postgres | Container port
CI host | 127.0.0.1 | Published port
CI service container | Service alias | Platform service config
Remote DB | Public/private DB host | Network allowlist
:::</p>
<h2>Keep migrations separate from readiness</h2>
<p>A database can be healthy before the schema is ready.</p>
<p>If your app needs migrations, make that an explicit pipeline step:</p>
<pre><code class="language-bash">docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm test
</code></pre>
<p>Or run tests inside a service that waits for both:</p>
<ul>
<li>database health</li>
<li>migrations complete</li>
<li>seed data loaded</li>
</ul>
<p>Otherwise you get a worse class of failure: intermittent test errors that look like app bugs but are really setup races.</p>
<h2>The debug output I want in every CI failure</h2>
<p>Do not dump secrets. Do print the shape of the environment.</p>
<p>Useful output:</p>
<ul>
<li>Docker Compose services and status</li>
<li>container logs for the dependency</li>
<li>resolved host and port, with password redacted</li>
<li>network names</li>
<li>health-check status</li>
<li>migration status</li>
</ul>
<p>Example:</p>
<pre><code class="language-bash">docker compose ps
docker compose logs --tail=80 postgres
docker network ls
</code></pre>
<p>The goal is to make the next failure diagnosable in one pass.</p>
<h2>The production lesson</h2>
<p>CI networking pain is a preview of production integration pain.</p>
<p>If your tests depend on hope, your deployments probably do too. Make service boundaries explicit. Add health checks. Split readiness from migrations. Log the right facts.</p>
<p>That is how you turn &quot;works on my machine&quot; into something a pipeline can prove.</p>
<p>:::offer-cta title=&quot;Need the pipeline cleaned up?&quot; label=&quot;next step&quot; href=&quot;/tools/route-finder&quot; cta=&quot;Find your route&quot;
Use the diagnostic to decide whether this is a focused audit sprint, a platform build, or an academy path you can work through yourself.
:::</p>
]]></content:encoded>
      <link>https://www.sageideas.dev/blog/fixing-docker-compose-connection-errors-in-ci-cd</link>
      <guid isPermaLink="true">https://www.sageideas.dev/blog/fixing-docker-compose-connection-errors-in-ci-cd</guid>
      <pubDate>Fri, 05 Jan 2024 00:00:00 GMT</pubDate>
      <category><![CDATA[CI/CD]]></category>
      <author>sage@sageideas.dev (Jason Teixeira)</author>
    </item>
  </channel>
</rss>