<?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: Tool Mango</title>
    <description>The latest articles on DEV Community by Tool Mango (@toolmango).</description>
    <link>https://dev.to/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>DEV Community: Tool Mango</title>
      <link>https://dev.to/toolmango</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolmango"/>
    <language>en</language>
    <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://dev.to/toolmango/i-cut-my-aws-bill-by-93-by-ditching-fargate-for-a-single-lightsail-vm-16lf</link>
      <guid>https://dev.to/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>
