<?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>Forem: Tool Mango</title>
    <description>The latest articles on Forem by Tool Mango (@toolmango).</description>
    <link>https://forem.com/toolmango</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3914742%2F3cd62fd3-ad3c-4cf7-aeb2-15f5f54414fd.png</url>
      <title>Forem: Tool Mango</title>
      <link>https://forem.com/toolmango</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/toolmango"/>
    <language>en</language>
    <item>
      <title>I dropped my SaaS Pro tier from $19 to $9 in 24 hours. Here's why.</title>
      <dc:creator>Tool Mango</dc:creator>
      <pubDate>Thu, 07 May 2026 14:14:04 +0000</pubDate>
      <link>https://forem.com/toolmango/i-dropped-my-saas-pro-tier-from-19-to-9-in-24-hours-heres-why-2ojl</link>
      <guid>https://forem.com/toolmango/i-dropped-my-saas-pro-tier-from-19-to-9-in-24-hours-heres-why-2ojl</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I shipped Pro at $19/mo, looked at it 24 hours later, dropped it to $9/mo, and felt instantly more honest about my product. Here's what changed about my pricing thinking — and the framework I use now.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I shipped first
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://toolmango.com" rel="noopener noreferrer"&gt;ToolMango&lt;/a&gt; — an AI tool directory with a Stack Builder that recommends a custom tool stack based on your project description. Pro tier was supposed to add: save stacks, label them, history dashboard, PDF export, email-the-stack, priority API queue, agency white-label, team workspace.&lt;/p&gt;

&lt;p&gt;I priced it at &lt;strong&gt;$19/mo&lt;/strong&gt; because that's the SaaS-indie default. It's where every other "AI thing" starts. ChatGPT is $20. Cursor is $20. Notion AI is $10. So $19 felt right.&lt;/p&gt;

&lt;p&gt;But here's what I actually shipped on day 1:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro features that worked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save stacks (dashboard list)&lt;/li&gt;
&lt;li&gt;Stack history (list all your stacks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro features I "promised" on the pricing page:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF export (not built)&lt;/li&gt;
&lt;li&gt;Email-the-stack (not built)&lt;/li&gt;
&lt;li&gt;Priority API queue (just marketing copy)&lt;/li&gt;
&lt;li&gt;Agency white-label (months away)&lt;/li&gt;
&lt;li&gt;Team workspace (months away)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest competitive comparison:&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;Product&lt;/th&gt;
&lt;th&gt;$/mo&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT Plus&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Frontier AI you'd use daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Replaces a junior engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion AI&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;AI inside an entire workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffer&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;Multi-account social scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carrd Pro&lt;/td&gt;
&lt;td&gt;$19/yr&lt;/td&gt;
&lt;td&gt;Entire website builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;My ToolMango Pro at launch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$19/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Save 5 stacks + history&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I was charging like Cursor and ChatGPT but delivering "saved bookmarks." Anyone who paid $19 and saw what they got would feel cheated within a week, then leave a bad review. That's the fastest way to kill a new product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I dropped to $9
&lt;/h2&gt;

&lt;p&gt;Three reasons, in order of importance:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Honest pricing reflects what you've actually built
&lt;/h3&gt;

&lt;p&gt;I was anchoring Pro to the &lt;em&gt;promise&lt;/em&gt; of features (PDF, email, white-label, priority) rather than the &lt;em&gt;delivery&lt;/em&gt; of features (save + history). That's bait-and-switch by another name.&lt;/p&gt;

&lt;p&gt;Pricing should reflect what a customer can use &lt;em&gt;the day they pay&lt;/em&gt;. Roadmap features are the reason someone &lt;em&gt;might&lt;/em&gt; pay later when they ship — not the reason to charge more now.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. $19 was a credibility leak
&lt;/h3&gt;

&lt;p&gt;In SaaS pricing, you anchor against what the user has bought before. They've bought ChatGPT at $20. They've bought Notion AI at $10. They know what those products feel like.&lt;/p&gt;

&lt;p&gt;If I show up at $19 with "save your stacks," they don't compare me to "save your stacks at $25." They compare me to "ChatGPT at $20." And lose.&lt;/p&gt;

&lt;p&gt;At $9, the comparison flips. They compare me to "Buffer at $6" and "Carrd at $19/yr." Now I look reasonable. The exact same product is positioned completely differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lower price compounds via word-of-mouth
&lt;/h3&gt;

&lt;p&gt;The single biggest source of indie SaaS growth at our scale is "my friend mentioned this tool." For that to happen, the price has to be at "I just signed up, you should too" levels — sub-$10/mo.&lt;/p&gt;

&lt;p&gt;At $19, the conversation is "is this worth $19?" At $9, the conversation is "this is cool, you should try it." The first is a sales conversation. The second is word-of-mouth. The second compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pricing ladder I committed to publicly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Today:        $9   saves + history + labels
+PDF export:  $12  real deliverable utility
+email send:  $14  workflow automation
+team/agency: $29  Agency tier; existing Pro stays $14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone who signed up at $9 today is grandfathered at $9 forever, even when prices rise. That solves two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loyalty signal&lt;/strong&gt; — early adopters get rewarded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No price-cap on revenue&lt;/strong&gt; — new signups pay current price&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Existing customers don't churn on price increases (because they're not affected), and new customers pay what the product is now worth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing framework I use now
&lt;/h2&gt;

&lt;p&gt;The four questions I ask before setting a price:&lt;/p&gt;

&lt;h3&gt;
  
  
  Q1: What can the customer use the day they pay?
&lt;/h3&gt;

&lt;p&gt;Not what's on the roadmap. What works. List those features only. Stop counting roadmap features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q2: What does this anchor against in the user's mental model?
&lt;/h3&gt;

&lt;p&gt;If they're thinking "this is like Notion AI, $10," start within 50% of that. If they're thinking "this is like a $200 enterprise tool," start higher. The user's frame matters more than your competitive analysis spreadsheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q3: At this price, would I recommend it to a friend?
&lt;/h3&gt;

&lt;p&gt;If I'm hesitating, it's too expensive. The friend recommendation is the marketing channel. Price has to enable it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q4: How does the price scale with what I ship next?
&lt;/h3&gt;

&lt;p&gt;If price stays the same forever, I never get to capture the value of what I'll ship later. If price rises with features, early adopters churn.&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;rising tiers + grandfathering&lt;/strong&gt;. Each new feature ships with a small price bump for new signups. Existing signups stay at their original price. Both groups feel like they're getting a deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math of grandfathering
&lt;/h2&gt;

&lt;p&gt;Counter-intuitively, grandfathering early customers at lower prices is more profitable than not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without grandfathering:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign up early adopter at $9&lt;/li&gt;
&lt;li&gt;Raise to $14 in 3 months&lt;/li&gt;
&lt;li&gt;Early adopter churns at month 3 because "the value isn't worth $14"&lt;/li&gt;
&lt;li&gt;You lose them forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With grandfathering:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign up early adopter at $9&lt;/li&gt;
&lt;li&gt;Raise to $14 for new customers in 3 months&lt;/li&gt;
&lt;li&gt;Early adopter stays at $9 forever, becomes a happy reference customer&lt;/li&gt;
&lt;li&gt;New customer pays $14, anchored against what current $9 customers experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LTV is much higher in the grandfathered version. And early adopters become your evangelists — they share screenshots of "I'm only paying $9, the new price is $14" which is basically free marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in code
&lt;/h2&gt;

&lt;p&gt;Three small updates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Price&lt;/strong&gt;: Created a new $9/mo Price in the same Product. Old $19 Price archived (still works for any test customers who already subscribed at $19).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/pricing&lt;/code&gt; page&lt;/strong&gt;: Updated copy. Listed only delivered features. Moved roadmap items to a clearly separated "On the roadmap" block with the disclosure that they're not yet built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard upgrade CTA&lt;/strong&gt;: Updated to mention the $9 founder rate and grandfathering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total change: ~50 lines of code, including the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper lesson
&lt;/h2&gt;

&lt;p&gt;Indie SaaS pricing is over-influenced by the "what should I charge?" question and under-influenced by the "what can I deliver?" question. The two need to match.&lt;/p&gt;

&lt;p&gt;If they're out of sync, the customers either feel ripped off (too high relative to delivery) or you leave money on the table (too low relative to delivery). The fix is honest scoping: what works &lt;em&gt;today&lt;/em&gt;, what's the closest competitive anchor, and where can the price grow as the product grows.&lt;/p&gt;

&lt;p&gt;I now think of pricing as a curve, not a number. The number changes as the curve moves. Customers grandfather on the curve they signed up at. Both sides win.&lt;/p&gt;




&lt;h2&gt;
  
  
  The site
&lt;/h2&gt;

&lt;p&gt;If you want to see what $9/mo Pro looks like in the wild: &lt;a href="https://toolmango.com" rel="noopener noreferrer"&gt;ToolMango&lt;/a&gt; is the planner. &lt;a href="https://toolmango.com/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt; shows the new positioning. &lt;a href="https://toolmango.com/about" rel="noopener noreferrer"&gt;/about&lt;/a&gt; explains the editorial ROI methodology.&lt;/p&gt;

&lt;p&gt;Roast the pricing in the comments — that's what dev.to is for.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>indiehackers</category>
      <category>startup</category>
      <category>saas</category>
    </item>
    <item>
      <title>I cut my AWS bill by 93% by ditching Fargate for a single Lightsail VM</title>
      <dc:creator>Tool Mango</dc:creator>
      <pubDate>Tue, 05 May 2026 21:51:22 +0000</pubDate>
      <link>https://forem.com/toolmango/i-cut-my-aws-bill-by-93-by-ditching-fargate-for-a-single-lightsail-vm-16lf</link>
      <guid>https://forem.com/toolmango/i-cut-my-aws-bill-by-93-by-ditching-fargate-for-a-single-lightsail-vm-16lf</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://toolmango.com" rel="noopener noreferrer"&gt;ToolMango&lt;/a&gt;, an AI tools directory, on AWS Fargate. The bill came back at &lt;strong&gt;$345/mo before traffic&lt;/strong&gt;. I migrated to a single $12 Lightsail VM in an afternoon and cut costs by &lt;strong&gt;93%&lt;/strong&gt; while keeping the same Next.js + Postgres + Redis + BullMQ stack alive.&lt;/p&gt;

&lt;p&gt;Here's exactly what I changed, what broke, and what I'd do differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  What ToolMango is (so the cost numbers make sense)
&lt;/h2&gt;

&lt;p&gt;ToolMango is an editorial directory of AI tools. It scores tools on an ROI Score (cost, time-to-value, output quality, free-tier generosity, category fit, reader engagement) and ranks them — &lt;em&gt;before&lt;/em&gt; knowing whether the tool has an affiliate program. Tools we don't earn from frequently outrank tools we do.&lt;/p&gt;

&lt;p&gt;Tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14 App Router&lt;/li&gt;
&lt;li&gt;Postgres 16&lt;/li&gt;
&lt;li&gt;Redis (BullMQ for the agent job queue)&lt;/li&gt;
&lt;li&gt;Anthropic Claude Sonnet for editorial agents (research, SEO sweep, social drafts)&lt;/li&gt;
&lt;li&gt;A worker process running 5 cron schedules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pre-revenue. Brand new domain. ~106 tools indexed at the time of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The original Fargate setup
&lt;/h2&gt;

&lt;p&gt;I started on AWS because I had CDK boilerplate from another project. The architecture was over-engineered for a directory site getting zero traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudFront → ALB → Fargate (web ×2 tasks, worker ×1)
                ↓
          Aurora Serverless v2 (writer)
          ElastiCache (Redis, t4g.small ×2)
          NAT ×2 (multi-AZ)
          VPC + interface endpoints
          WAF (managed rule sets)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CDK code is clean. It deploys with one command. It autoscales. It survives an AZ failure. It's exactly what a series-A SaaS would run.&lt;/p&gt;

&lt;p&gt;It's also $345/mo for &lt;strong&gt;zero users&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually costing money
&lt;/h2&gt;

&lt;p&gt;I broke it down with &lt;code&gt;aws ce get-cost-and-usage&lt;/code&gt; and a few &lt;code&gt;aws ecs describe-task-definition&lt;/code&gt; calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;$/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aurora Serverless v2 (no auto-pause, 0.5 ACU min)&lt;/td&gt;
&lt;td&gt;$86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fargate ARM64 (3 tasks: 2× web at 1vCPU/2GB + 1× worker at 0.5/1GB)&lt;/td&gt;
&lt;td&gt;$71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2× NAT Gateways (multi-AZ)&lt;/td&gt;
&lt;td&gt;$65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC interface endpoints (Secrets Manager × 3 AZ + others)&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB + WAF&lt;/td&gt;
&lt;td&gt;$34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch + Container Insights&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public IPv4 charges&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElastiCache (cache.t4g.small ×2 nodes)&lt;/td&gt;
&lt;td&gt;$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misc (CloudFront, Secrets, Route53, S3)&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The killer insight: &lt;strong&gt;about $87/mo of that bill is "infrastructure plumbing"&lt;/strong&gt; — NAT, ALB, ElastiCache, VPC endpoints. None of it is doing real work for the application. It's all there to support the architecture itself.&lt;/p&gt;

&lt;p&gt;That's the floor on a Fargate setup. For a pre-revenue project, it's nuts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Skeleton mode on AWS
&lt;/h2&gt;

&lt;p&gt;Before migrating, I tried to make Fargate cheap. CDK changes I shipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Aurora: enable auto-pause when idle&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cfnCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultChild&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CfnDBCluster&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;cfnCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serverlessV2ScalingConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;minCapacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// was 0.5 — auto-pause after 5 min idle&lt;/span&gt;
  &lt;span class="na"&gt;maxCapacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// was 4&lt;/span&gt;
  &lt;span class="na"&gt;secondsUntilAutoPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Network: 1 NAT instead of 2&lt;/span&gt;
&lt;span class="nl"&gt;natGateways&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// was 2 (multi-AZ)&lt;/span&gt;

&lt;span class="c1"&gt;// Web: smaller, fewer tasks, autoscale up if needed&lt;/span&gt;
&lt;span class="nx"&gt;desiredCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// was 2&lt;/span&gt;
&lt;span class="nx"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// was 1024&lt;/span&gt;
&lt;span class="nx"&gt;memoryLimitMiB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// was 2048&lt;/span&gt;

&lt;span class="c1"&gt;// Worker on Fargate Spot&lt;/span&gt;
&lt;span class="nx"&gt;capacityProviderStrategies&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;span class="na"&gt;capacityProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FARGATE_SPOT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;capacityProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FARGATE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="c1"&gt;// Container Insights off&lt;/span&gt;
&lt;span class="nx"&gt;containerInsightsV2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ContainerInsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// Backup retention&lt;/span&gt;
&lt;span class="nx"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// was 14&lt;/span&gt;

&lt;span class="c1"&gt;// WAF: removed entirely (CloudFront has free Shield Standard)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;$345/mo → ~$140/mo&lt;/strong&gt;. Better, but still ridiculous for a pre-revenue project.&lt;/p&gt;

&lt;p&gt;The reason it stopped at $140: NAT, ALB, ElastiCache, VPC endpoints, and Aurora storage all have hard floors. You can't make Fargate genuinely cheap because the architecture itself isn't designed for cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: The honest migration
&lt;/h2&gt;

&lt;p&gt;Lightsail is AWS's "give me a Linux VM and stop overthinking it" tier. $12/mo for 2 vCPU, 2GB RAM, 60GB SSD, 3TB transfer — and it includes a static IP and a firewall.&lt;/p&gt;

&lt;p&gt;The plan: run &lt;strong&gt;everything&lt;/strong&gt; on one VM in Docker Compose.&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;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:16-alpine&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="nv"&gt;./data/postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;/var/lib/postgresql/data&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;512M&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy noeviction&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="nv"&gt;./data/redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;/data&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;192M&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;web&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;tm-web:latest&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;127.0.0.1:3000:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;768M&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;worker&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;tm-worker:latest&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;384M&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For HTTPS termination: &lt;a href="https://caddyserver.com" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;, which auto-issues Let's Encrypt certs on first request. Configuration is one stanza:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;toolmango.com, www.toolmango.com {
    reverse_proxy 127.0.0.1:3000
    encode gzip zstd
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy reloads, Caddy gets the cert. Total setup time: 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Aurora data to local Postgres
&lt;/h2&gt;

&lt;p&gt;Aurora is in a private subnet (&lt;code&gt;PRIVATE_ISOLATED&lt;/code&gt;), so I couldn't &lt;code&gt;pg_dump&lt;/code&gt; from outside. The workaround: spin up a one-off ECS Fargate task in the existing Web's VPC that runs &lt;code&gt;pg_dump&lt;/code&gt; and uploads to S3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs run-task &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; tm-prod-compute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--task-definition&lt;/span&gt; tm-prod-pgdump &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--launch-type&lt;/span&gt; FARGATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-configuration&lt;/span&gt; &lt;span class="s1"&gt;'awsvpcConfiguration={subnets=[subnet-...],securityGroups=[sg-...],assignPublicIp=DISABLED}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task definition uses &lt;code&gt;postgres:16-alpine&lt;/code&gt;, installs &lt;code&gt;aws-cli&lt;/code&gt; on the fly, runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_dump &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-acl&lt;/span&gt; &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="nt"&gt;--if-exists&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;$DB_HOST&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nv"&gt;$DB_USER&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; toolmango &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/dump.sql.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/dump.sql.gz s3://tm-prod-assets/migration/dump.sql.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Lightsail VM, pull from S3 (via a presigned URL since Lightsail VMs don't have IAM roles by default), gunzip, and pipe into the local Postgres container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;gunzip&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; /tmp/dump.sql.gz | docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-U&lt;/span&gt; tmadmin &lt;span class="nt"&gt;-d&lt;/span&gt; toolmango
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;64 published tools transferred cleanly. ~485KB of data total (it's a directory site — barely any data).&lt;/p&gt;

&lt;h2&gt;
  
  
  Building images on the VM
&lt;/h2&gt;

&lt;p&gt;Lightsail is x86_64. Fargate was ARM64. So I had to rebuild for x86 anyway, which is a perfect excuse to build directly on the VM and skip the registry-push dance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;-f&lt;/span&gt; Dockerfile.web &lt;span class="nt"&gt;-t&lt;/span&gt; tm-web:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://toolmango.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_PLAUSIBLE_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;toolmango.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js builds need ~1.5-2GB peak memory. Lightsail's "small_3_0" has 2GB RAM. Tight, but adding 2GB swap solved it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 2G /swapfile
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /swapfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"/swapfile none swap sw 0 0"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First build: ~6 min. Subsequent builds with Docker layer cache: ~2 min. Acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tearing down AWS Fargate
&lt;/h2&gt;

&lt;p&gt;After Lightsail was serving traffic, I tore down the Fargate stacks via CDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Take final Aurora snapshot first (safety rollback)&lt;/span&gt;
aws rds create-db-cluster-snapshot &lt;span class="nt"&gt;--db-cluster-identifier&lt;/span&gt; ... &lt;span class="nt"&gt;--db-cluster-snapshot-identifier&lt;/span&gt; tm-prod-final-...

&lt;span class="c"&gt;# Disable deletion protection&lt;/span&gt;
aws rds modify-db-cluster &lt;span class="nt"&gt;--no-deletion-protection&lt;/span&gt; ...

&lt;span class="c"&gt;# Delete Aurora cluster + writer&lt;/span&gt;
aws rds delete-db-instance &lt;span class="nt"&gt;--skip-final-snapshot&lt;/span&gt; ...
aws rds delete-db-cluster &lt;span class="nt"&gt;--skip-final-snapshot&lt;/span&gt; ...

&lt;span class="c"&gt;# CDK destroy stacks in reverse dependency order&lt;/span&gt;
cdk destroy tm-prod-edge &lt;span class="nt"&gt;--force&lt;/span&gt;      &lt;span class="c"&gt;# CloudFront, WAF&lt;/span&gt;
cdk destroy tm-prod-compute &lt;span class="nt"&gt;--force&lt;/span&gt;   &lt;span class="c"&gt;# Fargate, ALB, ECS cluster&lt;/span&gt;
cdk destroy tm-prod-data &lt;span class="nt"&gt;--force&lt;/span&gt;      &lt;span class="c"&gt;# ElastiCache (S3 retains via RemovalPolicy.RETAIN)&lt;/span&gt;
cdk destroy tm-prod-network &lt;span class="nt"&gt;--force&lt;/span&gt;   &lt;span class="c"&gt;# VPC, NAT, subnets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudFront's destroy is the slowest — disabling a distribution then deleting it takes 15-20 min. Aurora delete is 5-10 min. Compute and network are 3-5 min each.&lt;/p&gt;

&lt;p&gt;Total teardown: ~30-40 min unattended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-deploy via GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The piece that ties it all together: a workflow that on push to main rsyncs the source, rebuilds images on the VM, runs Prisma migrations, restarts containers:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rsync source to Lightsail&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;rsync -az --delete --exclude='node_modules' --exclude='.next' --exclude='.git' \&lt;/span&gt;
      &lt;span class="s"&gt;-e "ssh -i ~/.ssh/id_ed25519" \&lt;/span&gt;
      &lt;span class="s"&gt;./ ${{ secrets.LIGHTSAIL_USER }}@${{ secrets.LIGHTSAIL_HOST }}:/home/ubuntu/toolmango/src/&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build images on Lightsail&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ssh ... 'cd /home/ubuntu/toolmango/src &amp;amp;&amp;amp; \&lt;/span&gt;
      &lt;span class="s"&gt;sg docker -c "docker build -f Dockerfile.web -t tm-web:latest ." &amp;amp;&amp;amp; \&lt;/span&gt;
      &lt;span class="s"&gt;sg docker -c "docker build -f Dockerfile.worker -t tm-worker:latest ."'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run prisma migrate + restart services&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ssh ... 'cd /home/ubuntu/toolmango &amp;amp;&amp;amp; \&lt;/span&gt;
      &lt;span class="s"&gt;sg docker -c "docker compose run --rm --no-deps web npx prisma migrate deploy" &amp;amp;&amp;amp; \&lt;/span&gt;
      &lt;span class="s"&gt;sg docker -c "docker compose up -d --force-recreate web worker"'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smoke test&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;for i in {1..6}; do&lt;/span&gt;
      &lt;span class="s"&gt;[ "$(curl -s -o /dev/null -w '%{http_code}' https://toolmango.com/api/healthz)" = "200" ] &amp;amp;&amp;amp; exit 0&lt;/span&gt;
      &lt;span class="s"&gt;sleep 5&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
    &lt;span class="s"&gt;exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First successful auto-deploy: 3m50s end-to-end. From &lt;code&gt;git push&lt;/code&gt; to verified &lt;code&gt;200 OK&lt;/code&gt; from Caddy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs now
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;$/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lightsail small_3_0 (2 vCPU, 2 GB RAM, 60 GB SSD, 3 TB transfer)&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 (assets bucket — kept)&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route53 (zone + queries)&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets Manager (kept for credentials backup, ~10 secrets)&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API (Claude Sonnet for editorial agents)&lt;/td&gt;
&lt;td&gt;$5–15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$23–33&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a 93% cut from $345/mo. Same site, same functionality, same automation pipeline. The site is at &lt;a href="https://toolmango.com" rel="noopener noreferrer"&gt;https://toolmango.com&lt;/a&gt; if you want to verify it's actually working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I gave up
&lt;/h2&gt;

&lt;p&gt;Honest list of what's worse on Lightsail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No multi-AZ HA.&lt;/strong&gt; Single VM = single point of failure. AZ-level outage means downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Aurora point-in-time restore.&lt;/strong&gt; Just nightly &lt;code&gt;pg_dump&lt;/code&gt; to S3. RPO is up to 24h. Acceptable for a content site, not for transactional data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No autoscaling.&lt;/strong&gt; Vertical only — bump to a bigger Lightsail bundle if traffic grows. The next tier is $24/mo for 4GB / 80GB. Past that, $44/mo for 8GB. At those numbers you should rethink Lightsail vs going back to managed services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual ops.&lt;/strong&gt; No service auto-restart on host failure. If the VM dies, I get notified by uptime monitor and SSH in. That's the trade.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skip Fargate entirely for pre-revenue projects.&lt;/strong&gt; Start on Lightsail. The migration was 4 hours; if I'd started there, that's 4 hours and ~$700 of avoided bills (the 6 days I was on Fargate).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't enable Container Insights "just because."&lt;/strong&gt; It's $5-15/mo and you'll never look at it on a small project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let CDK enable WAF by default.&lt;/strong&gt; WAF is real money ($12-15/mo) for a pre-revenue site that's not under attack. CloudFront's free Shield Standard is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't pre-provision multi-AZ NAT.&lt;/strong&gt; Single NAT is fine until you have customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Aurora &lt;code&gt;minCapacity: 0&lt;/code&gt; from day 1.&lt;/strong&gt; The auto-pause feature added in 2024 makes Aurora Serverless v2 actually serverless. Most CDK examples still default to 0.5.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to migrate back to Fargate
&lt;/h2&gt;

&lt;p&gt;The CDK code is still in the repo. &lt;code&gt;cdk deploy --all&lt;/code&gt; brings the production-grade Fargate stack back up; restore the latest Lightsail backup to a fresh Aurora cluster; cutover DNS.&lt;/p&gt;

&lt;p&gt;I'll do that when any of these hits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sustained traffic &amp;gt; 200 req/sec (single VM saturates)&lt;/li&gt;
&lt;li&gt;Need for multi-AZ HA (revenue at risk from single AZ outage)&lt;/li&gt;
&lt;li&gt;DB &amp;gt; 20 GB (Postgres on local SSD becomes risky for backup/recovery)&lt;/li&gt;
&lt;li&gt;Compliance requirement (SOC 2 etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until then, $25/mo. The CDK code waits patiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The site
&lt;/h2&gt;

&lt;p&gt;If you want to look at what this stack actually serves: &lt;a href="https://toolmango.com" rel="noopener noreferrer"&gt;ToolMango&lt;/a&gt;. The methodology behind the editorial ROI score is at &lt;a href="https://toolmango.com/about" rel="noopener noreferrer"&gt;/about&lt;/a&gt;, the affiliate disclosure is at &lt;a href="https://toolmango.com/disclosure" rel="noopener noreferrer"&gt;/disclosure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The writeup of the migration is in &lt;code&gt;docs/lightsail-migration.md&lt;/code&gt; in the repo if you want the step-by-step. Happy to answer questions in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful and you've done a similar AWS-to-cheap-VM migration, I'd love to hear what you cut and what burned.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>aws</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
