<?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: Cloud Frontier</title>
    <description>The latest articles on DEV Community by Cloud Frontier (@cloudfrontier).</description>
    <link>https://dev.to/cloudfrontier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027561%2F801991d0-b81c-4a26-99b0-b703e1d60dee.png</url>
      <title>DEV Community: Cloud Frontier</title>
      <link>https://dev.to/cloudfrontier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudfrontier"/>
    <language>en</language>
    <item>
      <title>Serverless: When It Helps and When It Hurts</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Tue, 01 Sep 2026 08:00:34 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/serverless-when-it-helps-and-when-it-hurts-3o05</link>
      <guid>https://dev.to/cloudfrontier/serverless-when-it-helps-and-when-it-hurts-3o05</guid>
      <description>&lt;h2&gt;
  
  
  The Hype and the Hangover
&lt;/h2&gt;

&lt;p&gt;Serverless is one of those buzzwords that promises to free you from infrastructure. "No servers to manage!" they say. And it's true, but only up to a point. I've built and operated serverless systems that were a joy, and others that were a nightmare. The difference wasn't the technology, it was whether I applied it where it actually fits.&lt;/p&gt;

&lt;p&gt;Let's be clear: serverless (FaaS like AWS Lambda, Google Cloud Functions, Azure Functions) is not a silver bullet. It's a tool with a very specific shape. Use it for the right problems and it's fantastic. Use it for the wrong ones and you'll be fighting it every step of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Helps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Spiky and Unpredictable Traffic
&lt;/h3&gt;

&lt;p&gt;If your workload has sudden bursts, long idle periods, or a pattern that's hard to forecast, serverless shines. You pay per invocation, so idle time costs almost nothing. A cron job that runs once a day? A webhook that gets hit a few times per hour? Perfect.&lt;/p&gt;

&lt;p&gt;For example, a small image resize endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AWS Lambda with Node.js&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resized&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No EC2 instance sitting there 24/7. No autoscaling group to configure. The platform scales to zero when idle and to a thousand concurrent requests when a viral post hits.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Event-Driven Processing
&lt;/h3&gt;

&lt;p&gt;Serverless is designed for events. File uploads, database changes, queue messages, IoT telemetry. You write a small function, connect it to an event source, and you're done. The plumbing is handled for you.&lt;/p&gt;

&lt;p&gt;For instance, processing a new upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AWS Lambda with Python, triggered by S3
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function runs exactly when the event happens, no polling, no waiting. That's the sweet spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Small, Independent Microservices
&lt;/h3&gt;

&lt;p&gt;If you have a service that does one thing, has a small codebase, and doesn't need to maintain long-lived connections, serverless is a great fit. It forces you to keep functions small and focused, which is good discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Hurts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Long-Running or Stateful Workloads
&lt;/h3&gt;

&lt;p&gt;Lambda has a 15-minute timeout. If you need to process a large file, run a complex computation, or maintain a WebSocket connection, you're going to hit a wall. You end up splitting work into chunks, coordinating state externally, and debugging distributed timeouts. That's pain you didn't need.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Low-Latency, High-Frequency Calls
&lt;/h3&gt;

&lt;p&gt;Cold starts are real. If your function is invoked a few times per second and each call needs to be under 50ms, serverless can be a problem. The first call after idle can take a second or more. You can work around it with provisioned concurrency, but that starts eating into the cost savings.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Complex Applications with Tight Coupling
&lt;/h3&gt;

&lt;p&gt;If your application is a monolith that shares in-memory state, serverless forces you to break that. You'll need external caches, databases, and message queues for everything. The overhead of managing that may be worse than just running a simple VM.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unexpected Costs at Scale
&lt;/h3&gt;

&lt;p&gt;Serverless pricing looks great at low volume. But when you're processing millions of requests, the per-invocation cost adds up. A long-running function that's called frequently can be more expensive than a dedicated instance. I've seen teams get a surprise bill because a function was written inefficiently (e.g., reading a large file into memory on every call).&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;p&gt;Ask yourself these questions before going serverless:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is the workload event-driven or request/response?&lt;/strong&gt; Event-driven fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the traffic predictable and steady?&lt;/strong&gt; If yes, a container or VM might be cheaper and simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need sub-second latency consistently?&lt;/strong&gt; Cold starts might kill you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can I break the work into small, stateless units?&lt;/strong&gt; If not, you'll fight the platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the total cost at peak?&lt;/strong&gt; Run the numbers for both options.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Serverless is not a religion. It's a tool. I've used it to build a video transcoding pipeline that only runs when someone uploads, saving 90% of the cost of a dedicated worker. I've also seen a team try to run a real-time chat app on Lambda and spend weeks fighting connection limits.&lt;/p&gt;

&lt;p&gt;Start small. Prototype a single function. Measure cold starts, latency, and cost. If it feels like you're bending the platform to your will, step back and consider a more traditional approach. The best architecture is the one that solves your problem without making you hate your job.&lt;/p&gt;

&lt;p&gt;Remember: "serverless" doesn't mean "no servers." It means "servers you don't have to think about." And sometimes, you absolutely should think about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;AWS Lambda Documentation&lt;/a&gt; - official docs for limits, pricing, and best practices.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/functions/docs" rel="noopener noreferrer"&gt;Google Cloud Functions Docs&lt;/a&gt; - if you're on GCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy building, and choose wisely.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Simple CI/CD Pipeline That Actually Works</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Sun, 30 Aug 2026 08:00:29 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/a-simple-cicd-pipeline-that-actually-works-2811</link>
      <guid>https://dev.to/cloudfrontier/a-simple-cicd-pipeline-that-actually-works-2811</guid>
      <description>&lt;h2&gt;
  
  
  The Problem with Over-Engineered Pipelines
&lt;/h2&gt;

&lt;p&gt;I've seen teams spend weeks building elaborate CI/CD pipelines with Kubernetes, multiple stages, and fancy dashboards. Then they realize the pipeline is harder to maintain than the app itself. The truth is, most projects don't need that. They need a pipeline that runs tests, builds an artifact, and deploys it somewhere. Here's a simple, reliable approach that works for small to medium projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Principles
&lt;/h2&gt;

&lt;p&gt;Before writing any config, keep these in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it linear&lt;/strong&gt;: One flow from commit to deploy, no branching logic unless absolutely necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail fast&lt;/strong&gt;: The pipeline should stop at the first failing step, so you know exactly what broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use versioned artifacts&lt;/strong&gt;: Always tag your builds with the commit SHA or a version number. It makes rollbacks trivial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make deployment idempotent&lt;/strong&gt;: Running the deploy step twice should be safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Pipeline Stages
&lt;/h2&gt;

&lt;p&gt;I'll use GitHub Actions as an example, but the same logic applies to GitLab CI, CircleCI, or Jenkins. The pipeline has four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt;, run unit tests and linting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt;, create a Docker image or a compiled binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt;, upload the artifact to a registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;, update the server or service.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A Minimal GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Here's a complete &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; that does all of this for a Node.js app deployed to a VPS via SSH.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&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;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&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 tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&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 Docker image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t myapp:${{ github.sha }} .&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;Push to registry&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;echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.example.com -u ${{ secrets.REGISTRY_USER }} --password-stdin&lt;/span&gt;
          &lt;span class="s"&gt;docker push myapp:${{ github.sha }}&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;Deploy to server&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_IP }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;docker pull myapp:${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;docker stop myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker rm myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker run -d --name myapp -p 80:3000 myapp:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single job&lt;/strong&gt;: Everything runs in one job, so you don't have to deal with artifact passing between jobs. It's slower but simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image tag by SHA&lt;/strong&gt;: The image is tagged with the commit SHA, so you always know what's running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy script is safe&lt;/strong&gt;: &lt;code&gt;docker stop&lt;/code&gt; and &lt;code&gt;docker rm&lt;/code&gt; are wrapped in &lt;code&gt;|| true&lt;/code&gt; so a missing container doesn't fail the deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets are managed&lt;/strong&gt;: No hardcoded credentials; everything is in GitHub secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handling Deploy Failures
&lt;/h2&gt;

&lt;p&gt;What if the deploy fails? The pipeline will show a red X, and you can check the logs. But you should also think about rollback. With SHA-tagged images, rollback is just a matter of re-running the deploy step with the previous SHA. You can even add a manual rollback job:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deploy'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rollback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Deploy specific tag&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_IP }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;docker pull myapp:${{ github.event.inputs.image_tag }}&lt;/span&gt;
            &lt;span class="s"&gt;docker stop myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker rm myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker run -d --name myapp -p 80:3000 myapp:${{ github.event.inputs.image_tag }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Add More Complexity
&lt;/h2&gt;

&lt;p&gt;This simple pipeline covers 80% of needs. But if you hit any of these, consider extending:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple environments&lt;/strong&gt;: Add a &lt;code&gt;staging&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; job, but keep them separate workflows triggered manually or by tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database migrations&lt;/strong&gt;: Run them as a separate step before deploy, but make them idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue-green or zero-downtime deploys&lt;/strong&gt;: Use a load balancer and swap containers, but that's a bigger change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Advice
&lt;/h2&gt;

&lt;p&gt;Start with the simplest thing that works. You can always add more later. The key is to make your pipeline reliable and boring. If you're spending more time on the pipeline than on the product, you're doing it wrong.&lt;/p&gt;

&lt;p&gt;I've used this exact pattern in several projects, and it's saved me tons of headaches. Try it, and you'll see how quickly you forget about deployments entirely.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ci</category>
      <category>cd</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cutting Cloud Costs with a Few Habits</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Fri, 28 Aug 2026 08:00:32 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-5fp2</link>
      <guid>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-5fp2</guid>
      <description>&lt;h2&gt;
  
  
  The Silent Budget Killer
&lt;/h2&gt;

&lt;p&gt;Cloud bills sneak up on you. One month you're paying $50, the next it's $500. The worst part? Most of that money goes to resources you forgot existed. I've been there, and I've learned that fixing cloud costs isn't about a big migration or buying reserved instances. It's about building a few simple habits that compound over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 1: Tag Everything from Day One
&lt;/h2&gt;

&lt;p&gt;Tags are the foundation of cost visibility. If you can't attribute a cost to a project, team, or environment, you can't make decisions about it. Start tagging every resource when you create it. Use a consistent scheme like &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt; (dev, staging, prod).&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;# Example: tagging an EC2 instance in AWS&lt;/span&gt;
aws ec2 create-tags &lt;span class="nt"&gt;--resources&lt;/span&gt; i-1234567890abcdef0 &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project,Value&lt;span class="o"&gt;=&lt;/span&gt;checkout-service &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;owner,Value&lt;span class="o"&gt;=&lt;/span&gt;payments-team &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;environment,Value&lt;span class="o"&gt;=&lt;/span&gt;prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have untagged resources, write a script to find them and add a &lt;code&gt;unknown&lt;/code&gt; tag. Then review that list monthly. Tags also enable budget alerts per project, so you can catch runaway spend early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 2: Set Budgets and Alerts (Then Actually Read Them)
&lt;/h2&gt;

&lt;p&gt;Most cloud providers let you set budgets and alerts. Configure them for your monthly spend and for each tagged project. Set alerts at 50%, 80%, and 100% of your budget. But here's the catch: alerts only work if you act on them. When you get a 80% alert, don't just acknowledge it. Spend 10 minutes checking what's driving the spike.&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;# AWS example: create a budget via CLI&lt;/span&gt;
aws budgets create-budget &lt;span class="nt"&gt;--account-id&lt;/span&gt; 123456789012 &lt;span class="nt"&gt;--budget&lt;/span&gt; file://budget.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;budget.json&lt;/code&gt;, define the amount and the alert thresholds. It's tedious to set up, but it's a one-time cost that pays off every month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 3: Schedule Non-Production Resources Off
&lt;/h2&gt;

&lt;p&gt;Development and staging environments don't need to run 24/7. If your team works 9-to-5, why are your dev servers running at 2 AM? Use instance schedules to stop resources outside working hours. Most cloud providers have native scheduling tools, or you can use a simple cron job with the CLI.&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;# Stop an EC2 instance at 7 PM UTC every weekday&lt;/span&gt;
0 19 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1-5 aws ec2 stop-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-1234567890abcdef0
&lt;span class="c"&gt;# Start it at 7 AM UTC&lt;/span&gt;
0 7 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1-5 aws ec2 start-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-1234567890abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This habit alone can cut dev costs by 60-70%. Just make sure you have a process to handle stateful data, like using snapshots before stopping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 4: Right-Size Before You Scale
&lt;/h2&gt;

&lt;p&gt;Before you add more instances, check if your current ones are over-provisioned. Cloud providers have tools like AWS Compute Optimizer or Azure Advisor that recommend sizes based on actual usage. But you can also do it manually: look at CPU and memory metrics over a week. If your instance is at 10% CPU, downgrade 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="c"&gt;# Check average CPU utilization for an instance (AWS)&lt;/span&gt;
aws cloudwatch get-metric-statistics &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/EC2 &lt;span class="nt"&gt;--metric-name&lt;/span&gt; CPUUtilization &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;InstanceId,Value&lt;span class="o"&gt;=&lt;/span&gt;i-1234567890abcdef0 &lt;span class="nt"&gt;--start-time&lt;/span&gt; 2024-01-01T00:00:00Z &lt;span class="nt"&gt;--end-time&lt;/span&gt; 2024-01-08T00:00:00Z &lt;span class="nt"&gt;--period&lt;/span&gt; 3600 &lt;span class="nt"&gt;--statistics&lt;/span&gt; Average
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right-sizing is not a one-time thing. Do it quarterly. Your workload changes, and your instances should too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 5: Review Inactive Resources Monthly
&lt;/h2&gt;

&lt;p&gt;We all forget to delete things. Old load balancers, unattached volumes, orphaned snapshots. These are silent money leaks. Make a monthly calendar reminder to check for resources with zero traffic or zero usage.&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;# List unattached EBS volumes (AWS)&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status,Values&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Volumes[*].{ID:VolumeId,Size:Size}'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete them. If you're nervous, take a final snapshot and delete after a week. You'll be surprised how much you find.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make It a Team Habit
&lt;/h2&gt;

&lt;p&gt;These habits work best when they're shared. Put cost reviews on your team's calendar. Share the monthly bill and discuss the top three costs. Make it a game: who can find the most wasted resources? The goal isn't to become a penny-pincher; it's to make sure every dollar you spend is actually helping your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Small, Win Big
&lt;/h2&gt;

&lt;p&gt;You don't need to implement all five habits at once. Pick one, like tagging or scheduling, and do it this week. Then add another next month. The cloud bill won't drop overnight, but in three months you'll see a real difference. And you'll never go back to the old way.&lt;/p&gt;

&lt;p&gt;Remember, cloud costs are not a mystery. They're a result of your habits. Change the habits, and you change the bill.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>aws</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>A Simple CI/CD Pipeline That Actually Works</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Tue, 25 Aug 2026 00:00:30 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/a-simple-cicd-pipeline-that-actually-works-17p2</link>
      <guid>https://dev.to/cloudfrontier/a-simple-cicd-pipeline-that-actually-works-17p2</guid>
      <description>&lt;h2&gt;
  
  
  The Problem with Most CI/CD Tutorials
&lt;/h2&gt;

&lt;p&gt;Most tutorials show you a pipeline that deploys a "hello world" app to a free Heroku instance. They skip the messy parts: secrets, rollbacks, and the moment your pipeline breaks because a dependency changed.&lt;/p&gt;

&lt;p&gt;I've been there. After years of fighting with over-engineered setups, I settled on a minimal pipeline that's easy to understand, debug, and extend. It's not fancy, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;A CI/CD pipeline is just three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; - run automated checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; - create an artifact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; - push the artifact to a server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll use GitHub Actions because it's free for public repos and integrates with everything. But the same concepts apply to GitLab CI, CircleCI, or Jenkins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline File
&lt;/h2&gt;

&lt;p&gt;Here's the complete &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&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;Deploy to server&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist/*"&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/www/myapp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Let's break it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: Test
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;test&lt;/code&gt; job runs on every push and pull request. It checks out the code, installs dependencies with &lt;code&gt;npm ci&lt;/code&gt; (which respects the lockfile), and runs your test suite.&lt;/p&gt;

&lt;p&gt;If a PR fails tests, the &lt;code&gt;build-and-deploy&lt;/code&gt; job won't run because of the &lt;code&gt;needs: test&lt;/code&gt; dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Build
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;build-and-deploy&lt;/code&gt; job only runs on pushes to &lt;code&gt;main&lt;/code&gt; (not on PRs). It builds your app into a &lt;code&gt;dist&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;For a Node.js app, &lt;code&gt;npm run build&lt;/code&gt; might be a bundler like Vite or webpack. For a Python app, you'd replace with &lt;code&gt;python -m build&lt;/code&gt; or similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Deploy
&lt;/h2&gt;

&lt;p&gt;The deployment step uses &lt;code&gt;scp&lt;/code&gt; to copy the built files to your server. It's dead simple and works for static sites, Node apps, or anything that runs behind a reverse proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets Management
&lt;/h2&gt;

&lt;p&gt;Never hardcode credentials. In GitHub, go to Settings &amp;gt; Secrets and Variables &amp;gt; Actions, and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SERVER_HOST&lt;/code&gt; - your server IP or domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SERVER_USER&lt;/code&gt; - SSH username (usually &lt;code&gt;deploy&lt;/code&gt; or &lt;code&gt;ubuntu&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt; - the private key for a dedicated deploy user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a separate user on your server with limited permissions. Don't use root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling Back
&lt;/h2&gt;

&lt;p&gt;Every deployment overwrites the previous &lt;code&gt;dist&lt;/code&gt; folder. That's fine for small apps, but if you break something, you need a quick rollback.&lt;/p&gt;

&lt;p&gt;I keep the last three releases on the server:&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;# On server, before deploying&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; /var/www/backups/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz /var/www/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then to rollback, just extract the backup. You can automate this with a script, but even manual is better than nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Dependencies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm ci&lt;/code&gt; installs exact versions from the lockfile. This prevents the "works on my machine" problem. For Python, use &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; with pinned versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Pipeline
&lt;/h2&gt;

&lt;p&gt;Before you commit, test locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both pass, commit and push to &lt;code&gt;main&lt;/code&gt;. Watch the Actions tab to see the pipeline run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSH key permissions&lt;/strong&gt;: Make sure the private key is in OpenSSH format, not PuTTY. If you generate with &lt;code&gt;ssh-keygen -t ed25519&lt;/code&gt;, it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path issues&lt;/strong&gt;: The &lt;code&gt;source: "dist/*"&lt;/code&gt; assumes your build outputs to &lt;code&gt;dist&lt;/code&gt;. Adjust for your project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server path permissions&lt;/strong&gt;: The deploy user needs write access to &lt;code&gt;/var/www/myapp&lt;/code&gt;. Set it up once with &lt;code&gt;sudo chown -R deploy:deploy /var/www/myapp&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Extending the Pipeline
&lt;/h2&gt;

&lt;p&gt;Once this works, you can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linting&lt;/strong&gt; - add a &lt;code&gt;lint&lt;/code&gt; job before tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database migrations&lt;/strong&gt; - run a separate job after deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; - send a Slack message on failure using &lt;code&gt;actions/slack-notify&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But don't add them until you need them. The beauty of this pipeline is its simplicity. When something breaks, you can trace the entire flow in five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This pipeline won't handle Kubernetes or blue-green deployments. But for most side projects, small business apps, and even some production systems, it's enough.&lt;/p&gt;

&lt;p&gt;Start simple. Get it working. Then iterate. That's the way to build something that actually works.&lt;/p&gt;

&lt;p&gt;Have you built a similar pipeline? What's your minimal setup? Let me know in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cutting Cloud Costs with a Few Habits</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Sun, 23 Aug 2026 00:01:19 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-4lbn</link>
      <guid>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-4lbn</guid>
      <description>&lt;h2&gt;
  
  
  The Silent Budget Killer
&lt;/h2&gt;

&lt;p&gt;Cloud bills creep up. You start with a small instance, add a database, enable logging, and before you know it, the monthly invoice looks like a mortgage payment. The good news: most overspending comes from habits, not architecture. Fix the habits, and you cut costs without a major refactor.&lt;/p&gt;

&lt;p&gt;I've trimmed my own cloud spend by about 40% using the practices below. They're boring, but they work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tag Everything, Then Enforce
&lt;/h2&gt;

&lt;p&gt;Tags are not just metadata. They are your cost accounting system. If you can't see which team, project, or environment owns a resource, you can't make decisions about it.&lt;/p&gt;

&lt;p&gt;Start with mandatory tags: &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;. Apply them to every resource, including storage buckets and load balancers. Then, enforce with a policy. On AWS, use Service Control Policies or a simple Lambda that stops resources missing tags. On GCP, use Org Policy. On Azure, Azure Policy.&lt;/p&gt;

&lt;p&gt;Once tags are in place, generate a cost report by tag. You'll immediately spot the "test" environment that's been running production-sized instances for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Right-Size, Then Schedule
&lt;/h2&gt;

&lt;p&gt;Most workloads don't need 24/7 compute. Development, staging, and even some production (like internal dashboards) can be shut down at night and on weekends.&lt;/p&gt;

&lt;p&gt;First, right-size: look at CPU and memory utilization over the last 30 days. If an instance peaks at 10% CPU, it's too big. Downgrade it. Don't guess; use the metrics.&lt;/p&gt;

&lt;p&gt;Then, schedule. Use a simple cron job or a cloud scheduler to stop instances at 7 PM and start them at 7 AM. For example, on AWS:&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;# Stop instances tagged env=dev at 7 PM UTC&lt;/span&gt;
eventbridge rule &lt;span class="nt"&gt;--schedule&lt;/span&gt; &lt;span class="s2"&gt;"cron(0 19 * * ? *)"&lt;/span&gt; &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a managed solution like AWS Instance Scheduler. The savings are immediate: a dev instance running 40 hours a week instead of 168 costs 76% less.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use Spot Instances for Stateless Workloads
&lt;/h2&gt;

&lt;p&gt;Spot instances (or preemptible VMs on GCP) can be 60-90% cheaper than on-demand. The catch: they can be reclaimed. So use them for workloads that tolerate interruption: batch jobs, CI runners, rendering, data processing.&lt;/p&gt;

&lt;p&gt;Set up a spot fleet with a mix of instance types. If one type gets reclaimed, another takes over. For CI, this is a no-brainer. Your builds are idempotent anyway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose for CI runner&lt;/span&gt;
&lt;span class="na"&gt;extension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;spot&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spot&lt;/span&gt;
  &lt;span class="na"&gt;max-price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just make sure your application handles graceful shutdown. Save state, retry, and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Set Budgets and Alerts
&lt;/h2&gt;

&lt;p&gt;You can't manage what you don't measure. Set a monthly budget per project or environment. Most clouds let you create budgets with alert thresholds: 50%, 90%, 100%.&lt;/p&gt;

&lt;p&gt;When you get an alert at 90%, you have a few days to act. Don't ignore it. The alert is your friend.&lt;/p&gt;

&lt;p&gt;Also, enable anomaly detection. Some tools (like AWS Cost Anomaly Detection) use machine learning to flag unusual spending. A sudden spike in data transfer or storage costs is often a misconfigured resource, not a business surge.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Delete Unused Resources
&lt;/h2&gt;

&lt;p&gt;This sounds obvious, but it's the biggest waste I see. Orphaned volumes, old snapshots, unattached IPs, stale load balancers. They all cost money.&lt;/p&gt;

&lt;p&gt;Set a monthly reminder to review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unattached EBS volumes (or persistent disks)&lt;/li&gt;
&lt;li&gt;Old snapshots beyond a retention period&lt;/li&gt;
&lt;li&gt;Elastic IPs not associated with an instance&lt;/li&gt;
&lt;li&gt;Load balancers with no targets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write a script to find them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=status,Values=available"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Volumes[*].VolumeId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then delete or snapshot. Make it a habit, not a one-time cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use Managed Services Wisely
&lt;/h2&gt;

&lt;p&gt;Managed services (RDS, Cloud SQL, DynamoDB) save engineering time, but they cost more than self-hosted alternatives. That's fine for production. But for low-traffic apps, a small managed database might be overkill.&lt;/p&gt;

&lt;p&gt;Consider serverless options: Aurora Serverless, Cloud Spanner, or even SQLite on a tiny instance. For many apps, a single small instance with PostgreSQL is enough and costs $15/month instead of $50 for a managed DB.&lt;/p&gt;

&lt;p&gt;Also, pay attention to data transfer costs. Egress is where clouds make money. Keep traffic within the same region and use a CDN for static assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Review Monthly, Act Weekly
&lt;/h2&gt;

&lt;p&gt;Cost optimization is not a one-time project. It's a habit. Set a recurring calendar event: every Monday, spend 15 minutes checking your cost dashboard.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New resources without tags&lt;/li&gt;
&lt;li&gt;Instances running 24/7 that shouldn't be&lt;/li&gt;
&lt;li&gt;Spike in data transfer&lt;/li&gt;
&lt;li&gt;Any resource that's been idle for 7 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make it a team ritual. Share the cost report. When everyone sees the numbers, they start thinking twice before spinning up a 16-core beast for a quick test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;These habits won't make your cloud bill zero, but they'll cut it significantly. The key is consistency. Tag everything, schedule what you can, use spot when possible, and review weekly.&lt;/p&gt;

&lt;p&gt;Start with one habit this week. Tag all resources. Next week, add a schedule. The savings compound. Your future self (and your finance team) will thank you.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>costoptimization</category>
      <category>aws</category>
    </item>
    <item>
      <title>Serverless: When It Helps and When It Hurts</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Fri, 21 Aug 2026 00:00:34 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/serverless-when-it-helps-and-when-it-hurts-25d1</link>
      <guid>https://dev.to/cloudfrontier/serverless-when-it-helps-and-when-it-hurts-25d1</guid>
      <description>&lt;h2&gt;
  
  
  The Allure of Serverless
&lt;/h2&gt;

&lt;p&gt;Serverless computing, despite its name, still runs on servers. The difference is that you don't manage them. You deploy functions, and the cloud provider handles scaling, patching, and availability. The promise is simple: you focus on code, not infrastructure. That's genuinely appealing for many projects, but it's not a silver bullet. Let's talk about when serverless shines and when it becomes a headache.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Helps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Spiky and Unpredictable Traffic
&lt;/h3&gt;

&lt;p&gt;Serverless scales automatically. If you have a sudden surge of users, functions spin up to handle the load, then scale down to zero when idle. You pay only for what you use. This is ideal for APIs with variable traffic, like a mobile app backend that sees daily peaks and quiet nights.&lt;/p&gt;

&lt;p&gt;For example, a simple REST endpoint using AWS Lambda and API Gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// process request&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No server to configure, no load balancer to set up. It just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Event-Driven Workloads
&lt;/h3&gt;

&lt;p&gt;Serverless excels at reacting to events: file uploads, database changes, messages in a queue. You can glue services together with minimal code. For instance, resizing an image when it's uploaded to S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;download_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="n"&gt;upload_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tmp/resized-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;download_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;download_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upload_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upload_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resized/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a perfect serverless use case: short-lived, stateless, and event-triggered.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reducing Operational Overhead
&lt;/h3&gt;

&lt;p&gt;For small teams or side projects, not having to patch servers, configure auto-scaling, or worry about high availability is a huge win. You can ship features faster because you're not spending time on infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Hurts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Long-Running Processes
&lt;/h3&gt;

&lt;p&gt;Most serverless providers have a maximum execution time. AWS Lambda defaults to 3 seconds, up to 15 minutes max. Google Cloud Functions can run up to 9 minutes. If you need to process large files, run complex computations, or handle streaming, you'll hit limits. You might be forced to split work into smaller chunks or use additional services like Step Functions, which adds complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cold Starts
&lt;/h3&gt;

&lt;p&gt;When a function hasn't been invoked for a while, the platform needs to initialize it: load your code, spin up a container, and run initialization. This can add 100-500ms latency, or more for Java or .NET. If you have a user-facing API, that delay can be noticeable. Mitigations exist (provisioned concurrency, keeping functions warm), but they cost extra and reduce the cost benefits.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cost at High, Constant Load
&lt;/h3&gt;

&lt;p&gt;If you have a steady stream of traffic, serverless can become more expensive than a traditional server. A dedicated VM that runs 24/7 might be cheaper than paying per invocation. For example, a function that runs 100 million times a month could rack up significant costs, especially if it uses memory or external services. You need to estimate your workload and compare pricing.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Debugging and Observability Challenges
&lt;/h3&gt;

&lt;p&gt;Distributed systems are hard to debug. With serverless, your code runs in ephemeral environments, and you can't SSH into a box. You rely on logging and tracing tools. If you have a complex workflow with multiple functions, tracing a request across them can be tricky. You often need to set up distributed tracing (like AWS X-Ray) and be disciplined about logging.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Vendor Lock-In
&lt;/h3&gt;

&lt;p&gt;Serverless is deeply tied to a cloud provider's ecosystem. You'll use their event sources, their SDKs, and their configuration. Moving to another provider or to a VM-based approach requires significant refactoring. If you want portability, you need to abstract your functions behind a common interface, which adds overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Decision
&lt;/h2&gt;

&lt;p&gt;Start by asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is my workload event-driven or does it have variable traffic? If yes, serverless is a strong candidate.&lt;/li&gt;
&lt;li&gt;Are my functions short-lived and stateless? Good.&lt;/li&gt;
&lt;li&gt;Do I have a team that can handle distributed debugging? If not, maybe stick to a monolith.&lt;/li&gt;
&lt;li&gt;Can I predict my traffic? If it's constant and high, a VM might be cheaper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serverless is a tool, not a religion. Use it where it fits, and don't force it where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I've used serverless for cron jobs, webhooks, and small APIs, and it's been great. But I've also seen teams struggle with timeouts and cold starts for their core product. The key is to understand the trade-offs and make an informed choice. Start small, prototype, and measure. You'll quickly find out if serverless is your friend or your enemy.&lt;/p&gt;

&lt;p&gt;For more details, check the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;AWS Lambda documentation&lt;/a&gt; or the &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview" rel="noopener noreferrer"&gt;Azure Functions overview&lt;/a&gt;. They have excellent resources to help you decide.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>cloud</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Cutting Cloud Costs with a Few Habits</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:01:06 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-2b1b</link>
      <guid>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-2b1b</guid>
      <description>&lt;h2&gt;
  
  
  The Silent Budget Killer
&lt;/h2&gt;

&lt;p&gt;Cloud bills creep up. You start with a small instance, a managed database, and a bucket. A year later, you're paying for resources you forgot existed. The worst part? Most of that waste is avoidable with a few simple habits.&lt;/p&gt;

&lt;p&gt;I've been there. After a particularly painful invoice, I made a checklist of practices that now keep my cloud spending in check. Here's what works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tag Everything from Day One
&lt;/h2&gt;

&lt;p&gt;Tags are not just for organization; they are your cost allocation superpower. Without tags, you can't answer the question: "What is this cost for?"&lt;/p&gt;

&lt;p&gt;Start tagging every resource with at least &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt; (dev, staging, prod). Most cloud providers let you enforce tags with policies. For example, in AWS, you can use a service control policy to deny creation of untagged resources.&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;# Example: tagging an EC2 instance with AWS CLI&lt;/span&gt;
aws ec2 create-tags &lt;span class="nt"&gt;--resources&lt;/span&gt; i-1234567890abcdef0 &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project,Value&lt;span class="o"&gt;=&lt;/span&gt;myapp &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;owner,Value&lt;span class="o"&gt;=&lt;/span&gt;team-x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once tags are in place, use the provider's cost explorer to group by tags. You'll immediately spot the expensive experiment that no one turned off.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Turn Off What You're Not Using
&lt;/h2&gt;

&lt;p&gt;This sounds obvious, but it's the most common leak. Developers spin up a server for testing and forget it. That server runs 24/7, costing money even when idle.&lt;/p&gt;

&lt;p&gt;Get into the habit of stopping (not terminating) instances when you're done. Even better, automate it. Use a simple script that stops instances outside business hours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: using boto3 to stop instances with a tag 'auto-stop'
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ec2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# outside 7am-7pm
&lt;/span&gt;    &lt;span class="n"&gt;instances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tag:auto-stop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For databases, consider stopping them too if your provider supports it (e.g., RDS can be stopped). For dev environments, use a schedule to start them in the morning and stop at night.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Right-Size Your Resources
&lt;/h2&gt;

&lt;p&gt;We tend to over-provision. A &lt;code&gt;t3.medium&lt;/code&gt; might be more than enough, but you chose &lt;code&gt;t3.large&lt;/code&gt; because you weren't sure. That doubles the cost.&lt;/p&gt;

&lt;p&gt;Review your usage metrics regularly. Cloud providers give you CPU, memory, and network stats. If utilization is consistently below 20% for a week, downsize. It's a quick change, and you can always scale back up if needed.&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;# Example: modify an EC2 instance type&lt;/span&gt;
aws ec2 modify-instance-attribute &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-123 &lt;span class="nt"&gt;--instance-type&lt;/span&gt; &lt;span class="s1"&gt;'{"Value": "t3.medium"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For managed services like databases, look at your storage and IOPS. Often you're paying for provisioned IOPS you never use.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use Serverless and Managed Services When It Makes Sense
&lt;/h2&gt;

&lt;p&gt;If you have a service that gets sporadic traffic, a serverless function (like Lambda) can be dramatically cheaper than a dedicated server. You pay per request and compute time, not for idle capacity.&lt;/p&gt;

&lt;p&gt;Similarly, managed services like Fargate for containers or Cloud Run for containers remove the need to manage servers. They scale to zero, so you pay nothing when there's no traffic.&lt;/p&gt;

&lt;p&gt;But be careful: serverless isn't always cheaper. If you have steady, predictable load, a reserved instance might be better. The key is to match the service to your traffic pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Set Budgets and Alerts
&lt;/h2&gt;

&lt;p&gt;You can't manage what you don't measure. Set a monthly budget for each project or environment. Most cloud providers have native budget tools that alert you when you're approaching the limit.&lt;/p&gt;

&lt;p&gt;For example, in AWS, you can create a budget with a threshold of 80% and 100%. You'll get an email when you hit those. Do the same for your other providers.&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;# Example: create a budget in AWS&lt;/span&gt;
aws budgets create-budget &lt;span class="nt"&gt;--account-id&lt;/span&gt; 123456789012 &lt;span class="nt"&gt;--budget&lt;/span&gt; file://budget.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it a habit to check your cost dashboard every Monday. Ten minutes can save you hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Clean Up Orphaned Resources
&lt;/h2&gt;

&lt;p&gt;When you delete an instance, its volumes might remain. Elastic IPs that aren't attached cost money. Snapshots pile up. These are the "zombie" resources that haunt your bill.&lt;/p&gt;

&lt;p&gt;Every month, go through your console and look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unattached volumes&lt;/li&gt;
&lt;li&gt;Unattached elastic IPs&lt;/li&gt;
&lt;li&gt;Old snapshots&lt;/li&gt;
&lt;li&gt;Load balancers with no instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Delete them. If you're using infrastructure as code, you can automate this with a script that lists and deletes orphaned resources, but be careful not to delete something in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Choose the Right Pricing Model
&lt;/h2&gt;

&lt;p&gt;For predictable workloads, reserved instances or savings plans can cut costs by up to 60%. You pay upfront or commit to a term, and you get a significant discount.&lt;/p&gt;

&lt;p&gt;If you have flexible workloads, spot instances can be up to 90% cheaper, but they can be interrupted. Use them for batch jobs or stateless workers.&lt;/p&gt;

&lt;p&gt;Don't just pay on-demand out of habit. Review your usage patterns and commit where it makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Habit Loop
&lt;/h2&gt;

&lt;p&gt;None of these are one-time fixes. They are habits. Tag as you go. Stop when done. Review monthly. The payoff is a predictable bill and money back in your pocket.&lt;/p&gt;

&lt;p&gt;Start with one habit today. Your future self will thank you.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>devops</category>
      <category>cost</category>
    </item>
    <item>
      <title>Docker Images Without the Bloat</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Sat, 15 Aug 2026 16:03:18 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/docker-images-without-the-bloat-15jd</link>
      <guid>https://dev.to/cloudfrontier/docker-images-without-the-bloat-15jd</guid>
      <description>&lt;h2&gt;
  
  
  Why Size Matters
&lt;/h2&gt;

&lt;p&gt;When I started with Docker, I treated images like VMs: install everything, run everything, ship it. My first production image was over 1.5 GB. Deploys were slow, registry costs were climbing, and pulling an image on a bad connection was a nightmare. I learned the hard way that image size isn't just a vanity metric. It directly affects build time, push/pull speed, disk usage, and attack surface. Smaller images are faster to ship and easier to secure.&lt;/p&gt;

&lt;p&gt;The good news: cutting bloat is mostly about discipline and a few clever tricks. Here's what actually worked for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Right Base
&lt;/h2&gt;

&lt;p&gt;Your base image sets the floor. Don't grab &lt;code&gt;ubuntu:latest&lt;/code&gt; when you need just a runtime. For Node.js, I use &lt;code&gt;node:alpine&lt;/code&gt; (about 50 MB vs 300+ MB for the full image). For Python, &lt;code&gt;python:slim&lt;/code&gt; is a solid middle ground. Alpine uses musl instead of glibc, which can break some native packages, so test early.&lt;/p&gt;

&lt;p&gt;If you're really minimal, you can even use &lt;code&gt;scratch&lt;/code&gt; as a base for static binaries. I've built Go apps that run on &lt;code&gt;scratch&lt;/code&gt; with just the binary and a CA certificate file. That's a 10 MB image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Multi-Stage Builds
&lt;/h2&gt;

&lt;p&gt;This is the single biggest win. Instead of one giant Dockerfile, split it into stages: a build stage with all the compilers and dev dependencies, and a runtime stage that only copies the artifacts.&lt;/p&gt;

&lt;p&gt;Here's a typical Node example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 2: runtime&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the runtime stage doesn't include the source code or dev dependencies. The final image only has what's needed to run.&lt;/p&gt;

&lt;p&gt;For a Go app, it's even cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; app .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/app /app&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static linking means no libc, no shell, nothing extra. That's the dream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean Up After Yourself
&lt;/h2&gt;

&lt;p&gt;Package managers leave caches. &lt;code&gt;apt-get install&lt;/code&gt; leaves &lt;code&gt;/var/lib/apt/lists&lt;/code&gt;. &lt;code&gt;npm install&lt;/code&gt; leaves &lt;code&gt;~/.npm&lt;/code&gt;. &lt;code&gt;pip install&lt;/code&gt; leaves &lt;code&gt;~/.cache/pip&lt;/code&gt;. Always clean up in the same RUN layer to avoid bloating the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For npm, use &lt;code&gt;npm ci --omit=dev&lt;/code&gt; and then &lt;code&gt;npm cache clean --force&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mind the .dockerignore
&lt;/h2&gt;

&lt;p&gt;If you're not using a &lt;code&gt;.dockerignore&lt;/code&gt; file, you're probably copying &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, and other junk into your build context. That slows down builds and can accidentally leak secrets. Here's a minimal one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
&lt;span class="n"&gt;dist&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;
*.&lt;span class="n"&gt;log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Combine RUN Commands
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;RUN&lt;/code&gt; creates a new layer. Layers are cached, but every file that's deleted in a later layer still exists in the previous one. If you delete a temp file in a separate &lt;code&gt;RUN&lt;/code&gt;, the image still contains that file in the layer below. So chain commands with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and clean up in the same layer.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use --squash (or Not)
&lt;/h2&gt;

&lt;p&gt;Docker has an experimental &lt;code&gt;--squash&lt;/code&gt; flag that merges all layers into one. It can help, but it also destroys layer caching and makes rebuilds slower. I rarely use it in CI. Instead, I rely on multi-stage builds and careful layer design. If you're desperate, you can use &lt;code&gt;docker-slim&lt;/code&gt; or similar tools, but they're not magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check What's Actually In There
&lt;/h2&gt;

&lt;p&gt;After you build, inspect the image with &lt;code&gt;docker history&lt;/code&gt; to see layer sizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;my-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll quickly spot the huge layers. Also use &lt;code&gt;docker image inspect&lt;/code&gt; to see entrypoint, exposed ports, etc. For a deeper dive, &lt;code&gt;dive&lt;/code&gt; is a great tool (though I can't link it here, just search for it). It shows you exactly what each layer adds and flags unused files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realistic Example
&lt;/h2&gt;

&lt;p&gt;I once had a Python service that was 900 MB. After switching to &lt;code&gt;python:slim&lt;/code&gt;, using multi-stage, and removing pip caches, it dropped to 180 MB. Then I moved to a custom build that only copied the virtualenv and the app code, and it went to 120 MB. Not bad for a service that uses pandas and numpy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Image bloat isn't inevitable. It's a habit. Start with a small base, use multi-stage builds, clean caches, and ignore junk. Your CI will thank you, your registry costs will drop, and your deployments will feel snappy. Plus, fewer packages means fewer vulnerabilities to patch. It's a win on every axis.&lt;/p&gt;

&lt;p&gt;Next time you're about to &lt;code&gt;apt-get install&lt;/code&gt; something, ask yourself: do I really need this in the final image? Most of the time, the answer is no.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Slim Down Your Docker Images Without the Bloat</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:05:25 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/slim-down-your-docker-images-without-the-bloat-2p84</link>
      <guid>https://dev.to/cloudfrontier/slim-down-your-docker-images-without-the-bloat-2p84</guid>
      <description>&lt;h2&gt;
  
  
  Why Your Images Are Fat
&lt;/h2&gt;

&lt;p&gt;Every layer in a Docker image adds size. If you start with a full OS image, install build tools, then copy in your app, you're shipping a lot of unnecessary weight. This slows down CI/CD, increases storage costs, and makes pulls painful.&lt;/p&gt;

&lt;p&gt;The good news: you can cut image size dramatically with a few techniques. Here's what I use daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start Minimal
&lt;/h2&gt;

&lt;p&gt;Choose a small base image. &lt;code&gt;alpine&lt;/code&gt; is a classic, but &lt;code&gt;distroless&lt;/code&gt; and &lt;code&gt;slim&lt;/code&gt; variants are even leaner. For Node.js, &lt;code&gt;node:alpine&lt;/code&gt; is a solid start. For Python, &lt;code&gt;python:alpine&lt;/code&gt; works well. If you need glibc (some native modules), consider &lt;code&gt;debian:bullseye-slim&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Multi-Stage Builds
&lt;/h2&gt;

&lt;p&gt;This is the biggest win. Use one stage to build and another to run. The final image only contains what's needed to run, not the compiler or build tools.&lt;/p&gt;

&lt;p&gt;Example for a Go app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go build &lt;span class="nt"&gt;-o&lt;/span&gt; myapp .

&lt;span class="c"&gt;# Run stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/myapp /usr/local/bin/myapp&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final image is just Alpine plus your binary. No Go toolchain.&lt;/p&gt;

&lt;p&gt;For Node.js, you can do similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Run stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Combine RUN Commands
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;RUN&lt;/code&gt; creates a layer. Combine commands with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; to reduce layers. Also clean up package manager caches in the same step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Alpine, use &lt;code&gt;apk&lt;/code&gt; with &lt;code&gt;--no-cache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Use .dockerignore
&lt;/h2&gt;

&lt;p&gt;Your build context is sent to the daemon. Exclude node_modules, .git, logs, and other junk. This speeds up builds and prevents accidental copies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
*.&lt;span class="n"&gt;log&lt;/span&gt;
.&lt;span class="n"&gt;DS_Store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Copy Only What You Need
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;COPY . .&lt;/code&gt;, copy specific files. This also helps layer caching: if only your code changes, the dependencies layer stays cached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Consider Distroless
&lt;/h2&gt;

&lt;p&gt;For production, &lt;code&gt;distroless&lt;/code&gt; images are super minimal and secure (no shell). They force you to handle signals properly. Example for Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
...

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian11&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/myapp /myapp&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Squash Layers (Careful)
&lt;/h2&gt;

&lt;p&gt;You can squash layers with &lt;code&gt;docker build --squash&lt;/code&gt; (experimental) or tools like &lt;code&gt;docker-slim&lt;/code&gt;. But usually multi-stage is enough. I rarely need to squash.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Check Your Size
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;docker images&lt;/code&gt; to see sizes. For a detailed breakdown, &lt;code&gt;docker history&lt;/code&gt; shows layer sizes. Tools like &lt;code&gt;dive&lt;/code&gt; are great for interactively inspecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;I once had a Node.js image at 1.2GB. After multi-stage and switching to Alpine, it dropped to 180MB. The build time also improved because we weren't installing dev dependencies in the final image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Start with a small base, use multi-stage builds, combine commands, and use .dockerignore. These habits will keep your images lean and your deployments fast. It's not about micro-optimizing every byte; it's about shipping only what's necessary.&lt;/p&gt;

&lt;p&gt;For more, check the &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/" rel="noopener noreferrer"&gt;Docker best practices&lt;/a&gt; official guide.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Docker Images Without the Bloat</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:01:15 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/docker-images-without-the-bloat-g7a</link>
      <guid>https://dev.to/cloudfrontier/docker-images-without-the-bloat-g7a</guid>
      <description>&lt;h2&gt;
  
  
  Stop Shipping the Kitchen Sink
&lt;/h2&gt;

&lt;p&gt;We've all been there: you build a Docker image for a simple Python script, and it's 800MB. You pull it on a slow connection and watch the progress bar crawl. The image contains compilers, headers, and a full OS package manager you'll never use. It's time to trim the fat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Right Base
&lt;/h2&gt;

&lt;p&gt;Your base image sets the ceiling. &lt;code&gt;ubuntu:latest&lt;/code&gt; is a convenience, not a goal. For most apps, you can go much smaller.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;alpine&lt;/code&gt; is tiny (around 5MB) and uses musl, but sometimes native modules need extra work.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debian:bookworm-slim&lt;/code&gt; is a middle ground: Glibc, but without the bloat.&lt;/li&gt;
&lt;li&gt;For Go or Rust, consider &lt;code&gt;scratch&lt;/code&gt; or &lt;code&gt;distroless&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a quick comparison for a simple Python app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="c"&gt;# ~120MB&lt;/span&gt;

&lt;span class="c"&gt;# python:3.12-alpine&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-alpine&lt;/span&gt;
&lt;span class="c"&gt;# ~50MB&lt;/span&gt;

&lt;span class="c"&gt;# python:3.12 (full)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12&lt;/span&gt;
&lt;span class="c"&gt;# ~1GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always check the official images for slim or alpine variants. They exist for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Stage Builds: The Killer Feature
&lt;/h2&gt;

&lt;p&gt;If you need compilers to build, don't ship them. Use a multi-stage build: compile in one stage, copy only the artifacts to a clean final stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; myapp .

&lt;span class="c"&gt;# Final stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:latest&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; ca-certificates
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/myapp /usr/local/bin/myapp&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Go binary is static, so we copy it into a minimal Alpine. Final image size? Around 10MB. Compare that to shipping the Go toolchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean Up Within a Layer
&lt;/h2&gt;

&lt;p&gt;If you're stuck with a single-stage build (legacy, or just quick), at least clean up in the same &lt;code&gt;RUN&lt;/code&gt; command. Each &lt;code&gt;RUN&lt;/code&gt; creates a layer, and files deleted in a later layer still exist in the previous one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get purge &lt;span class="nt"&gt;-y&lt;/span&gt; build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get autoremove &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chain: this prevents intermediate layers from holding onto temporary files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use .dockerignore
&lt;/h2&gt;

&lt;p&gt;Your build context can be huge if you're not careful. A stray &lt;code&gt;node_modules&lt;/code&gt; or &lt;code&gt;.git&lt;/code&gt; folder gets sent to the daemon, slowing builds and bloating cache. Add a &lt;code&gt;.dockerignore&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;git&lt;/span&gt;
&lt;span class="n"&gt;node_modules&lt;/span&gt;
&lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;pycache__&lt;/span&gt;
*.&lt;span class="n"&gt;md&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Copy Specific Files
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;COPY . .&lt;/code&gt;, copy only what you need. This also helps with layer caching: if only your code changes, you don't invalidate the layer with dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src ./src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Don't Install Unnecessary Packages
&lt;/h2&gt;

&lt;p&gt;That's obvious, but also think about package manager caches. &lt;code&gt;pip install&lt;/code&gt; leaves &lt;code&gt;.pyc&lt;/code&gt; files and caches. Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For npm, &lt;code&gt;npm ci --only=production&lt;/code&gt; skips devDependencies and uses the lockfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distroless and Scratch
&lt;/h2&gt;

&lt;p&gt;For production, consider &lt;code&gt;gcr.io/distroless&lt;/code&gt; images. They have no shell, no package manager, just your app and runtime. This reduces attack surface and size.&lt;/p&gt;

&lt;p&gt;If your app is fully static (Go, Rust), you can use &lt;code&gt;scratch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/myapp /myapp&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's as small as it gets: just the binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Your Layers
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;docker history&lt;/code&gt; to see what's taking space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;myimage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll spot layers that are unexpectedly large. Also, &lt;code&gt;docker images&lt;/code&gt; shows the total size, but remember that shared layers between images don't count twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Slimming Down a Python API
&lt;/h2&gt;

&lt;p&gt;Here's a before and after for a FastAPI app.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "main:app", "--host", "0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Size: ~1GB&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/local/bin /usr/local/bin&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "main:app", "--host", "0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Size: ~150MB. The trick is copying only the installed packages, not the entire builder image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Bloat isn't just about download speed. Smaller images mean faster startups, less disk usage, and fewer security vulnerabilities. Start with a slim base, use multi-stage builds, and always clean up. Your future self (and your CI pipeline) will thank you.&lt;/p&gt;

&lt;p&gt;Happy trimming!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cutting Cloud Costs with a Few Habits</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Sat, 08 Aug 2026 08:00:28 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-4b3e</link>
      <guid>https://dev.to/cloudfrontier/cutting-cloud-costs-with-a-few-habits-4b3e</guid>
      <description>&lt;h2&gt;
  
  
  The Cloud Bill Isn't a Mystery
&lt;/h2&gt;

&lt;p&gt;Every month, the same shock. The cloud bill arrives, and it's higher than expected. I used to blame the provider, then my team, then the phase of the moon. But after years of cleaning up messes, I realized the problem isn't the cloud - it's our habits. A few small changes in how we deploy, monitor, and think about resources can cut costs dramatically without sacrificing performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 1: Right-Size Everything, Regularly
&lt;/h2&gt;

&lt;p&gt;Your instances are probably oversized. We tend to pick a size that feels safe, then never revisit it. I've seen production boxes running at 10% CPU for months. That's money burning for no reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit:&lt;/strong&gt; Every quarter, check your usage metrics. Look at CPU, memory, and network. If an instance has been under 20% utilization for 30 days, downgrade it. Automate this if you can - some providers offer rightsizing recommendations.&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;# Quick check with AWS CLI (example)&lt;/span&gt;
aws cloudwatch get-metric-statistics &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/EC2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; CPUUtilization &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;InstanceId,Value&lt;span class="o"&gt;=&lt;/span&gt;i-1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; 2024-01-01T00:00:00Z &lt;span class="nt"&gt;--end-time&lt;/span&gt; 2024-01-31T00:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 86400 &lt;span class="nt"&gt;--statistics&lt;/span&gt; Average
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using Kubernetes, look at &lt;code&gt;kubectl top nodes&lt;/code&gt; and adjust your resource requests. The goal is to match capacity to actual demand, not to your fear of a spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 2: Turn Off What You're Not Using
&lt;/h2&gt;

&lt;p&gt;Development and staging environments are the worst offenders. They run 24/7, but nobody touches them after 6 PM. I once found a test database that hadn't been queried in three months. It was costing $400 a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit:&lt;/strong&gt; Implement a schedule. Shut down non-production resources outside business hours. Use a simple cron job or a cloud function to stop instances at 7 PM and start them at 7 AM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: AWS Lambda with boto3 to stop instances tagged 'dev' after hours
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop_dev_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ec2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tag:Environment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Reservations&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Instances&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;State&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InstanceIds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stopped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it a policy: if a resource isn't used for a week, it gets deleted or stopped. You can always start it again later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 3: Use Managed Services Wisely
&lt;/h2&gt;

&lt;p&gt;Managed services like RDS, ElastiCache, or Cloud SQL are convenient, but they come with a premium. Sometimes that premium is worth it. Often, it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit:&lt;/strong&gt; For every managed service, ask: "Can I run this myself with a small VM?" If the answer is yes and you have the operational capacity, consider self-hosting. For example, a small PostgreSQL database on a t3.micro might cost $15/month, while RDS for the same workload could be $30.&lt;/p&gt;

&lt;p&gt;But don't go overboard. If you have a team that can't handle patching, backups, and failover, the managed service is worth the extra cost. The key is to make a conscious choice, not a default one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 4: Set Budgets and Alerts
&lt;/h2&gt;

&lt;p&gt;You can't manage what you don't measure. I've seen teams get a $10,000 surprise because they never set up billing alerts. That's a painful way to learn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit:&lt;/strong&gt; Set a monthly budget for each project or environment. Configure alerts at 50%, 80%, and 100% of that budget. Most cloud providers have this built in. If you're on AWS, use Budgets; on GCP, use Budgets and Alerts; on Azure, use Cost Management.&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;# AWS CLI example: create a budget alert&lt;/span&gt;
aws budgets create-budget &lt;span class="nt"&gt;--account-id&lt;/span&gt; 123456789012 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--budget&lt;/span&gt; &lt;span class="s1"&gt;'{"BudgetName":"monthly-prod","BudgetLimit":{"Amount":"5000","Unit":"USD"},"TimeUnit":"MONTHLY","BudgetType":"COST"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notifications&lt;/span&gt; &lt;span class="s1"&gt;'[{"NotificationType":"ACTUAL","ComparisonOperator":"GREATER_THAN","Threshold":80,"ThresholdType":"PERCENTAGE","NotificationState":"ALARM"}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an alert fires, don't ignore it. Investigate immediately. Usually, it's a runaway resource or a forgotten instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 5: Review Your Storage and Snapshots
&lt;/h2&gt;

&lt;p&gt;Storage is sneaky. You take a snapshot once, and then it grows forever. Old backups pile up. Orphaned volumes sit there charging you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit:&lt;/strong&gt; Once a month, list all your storage resources. Delete snapshots older than 30 days unless you have a compliance reason to keep them. Remove unattached volumes. Use lifecycle policies to automate this.&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;# List unattached EBS volumes (AWS example)&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status,Values&lt;span class="o"&gt;=&lt;/span&gt;available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For object storage like S3, enable lifecycle rules to transition old files to cheaper tiers (like Glacier) or delete them after a set period.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost of Bad Habits
&lt;/h2&gt;

&lt;p&gt;These aren't one-time fixes. They're habits. The first month you'll save a little. The second month, more. Over a year, you'll be surprised at how much you've cut. But the real benefit is the mindset shift: you start thinking about cost as a first-class citizen, not an afterthought.&lt;/p&gt;

&lt;p&gt;I've seen teams cut their cloud bill by 40% just by doing these five things consistently. No magic, no expensive software. Just habits.&lt;/p&gt;

&lt;p&gt;Start with one. Pick the one that hurts the most, and make it a routine. Your future self (and your finance team) will thank you.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>costoptimization</category>
      <category>aws</category>
    </item>
    <item>
      <title>AWS S3 basics every dev should know</title>
      <dc:creator>Cloud Frontier</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:05:15 +0000</pubDate>
      <link>https://dev.to/cloudfrontier/aws-s3-basics-every-dev-should-know-1lfh</link>
      <guid>https://dev.to/cloudfrontier/aws-s3-basics-every-dev-should-know-1lfh</guid>
      <description>&lt;h2&gt;
  
  
  Why S3 matters
&lt;/h2&gt;

&lt;p&gt;Amazon S3 is the object storage service that powers a huge part of the internet. If you build anything that stores files, serves static assets, or handles uploads, you'll likely touch S3. It's simple in theory: buckets hold objects (files), and each object has a key (path). But the details matter, especially around permissions, consistency, and cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buckets and keys
&lt;/h2&gt;

&lt;p&gt;A bucket is a global namespace. Bucket names must be unique across all AWS accounts, so you can't just name yours &lt;code&gt;uploads&lt;/code&gt;. Use a prefix like &lt;code&gt;mycompany-uploads&lt;/code&gt;. Keys are just strings; they can contain slashes to mimic folders, but S3 doesn't really have folders. That's why listing objects with a prefix is the way to "browse" a directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://mycompany-uploads/images/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lists all objects whose key starts with &lt;code&gt;images/&lt;/code&gt;. Empty "folders" don't exist unless you create a zero-byte object with a trailing slash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions are the hard part
&lt;/h2&gt;

&lt;p&gt;By default, everything is private. You control access with IAM policies for users/roles, bucket policies for cross-account or public access, and ACLs (legacy, avoid). The most common mistake is making a bucket public when you only need a few objects public. Instead, use a bucket policy to allow &lt;code&gt;s3:GetObject&lt;/code&gt; on a specific prefix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::mycompany-uploads/public/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For uploads from a web app, never give the client full bucket access. Use presigned URLs: generate a temporary URL that allows a specific PUT or GET for a limited time. Your backend signs the request, so you control size, expiration, and path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_presigned_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;put_object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mycompany-uploads&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user-123/photo.jpg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;ExpiresIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Consistency: strong now
&lt;/h2&gt;

&lt;p&gt;S3 used to have eventual consistency for reads after writes, but since 2020 it's strongly consistent for all operations. That means you can write an object and immediately read it, list it, or overwrite it. No more "wait a second" hacks. This simplifies a lot of logic, but still be careful with concurrent writes: last write wins, so use versioning if you need history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage classes and lifecycle
&lt;/h2&gt;

&lt;p&gt;Not all data needs the same durability or access speed. S3 has several classes: Standard for frequent access, Intelligent-Tiering for unknown patterns, Glacier for archives. You can set lifecycle rules to transition objects automatically. For example, move logs to Standard-IA after 30 days, then to Glacier after a year.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ArchiveLogs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Transitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD_IA"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLACIER"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saves money without manual effort. But beware: retrieving from Glacier takes minutes to hours, so don't put hot data there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning and deletion
&lt;/h2&gt;

&lt;p&gt;Enable versioning on buckets that hold important data. It protects against accidental overwrites and deletes. When you delete an object, S3 adds a delete marker instead of removing the data. You can always recover previous versions. This is a lifesaver for production.&lt;/p&gt;

&lt;p&gt;However, versioning doubles your storage cost because every version is stored. Combine it with lifecycle rules to clean up old versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost gotchas
&lt;/h2&gt;

&lt;p&gt;S3 pricing seems cheap, but it adds up. The main costs are storage, requests, and data transfer. Request costs are per 1000 operations, so high-frequency small reads can be more expensive than the storage itself. Use CloudFront as a CDN in front of S3 to reduce requests and data transfer costs. Also, avoid listing buckets frequently in code; that's a request-heavy operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Block public access unless absolutely necessary.&lt;/li&gt;
&lt;li&gt;Enable server-side encryption (SSE-S3 is free, or use KMS for more control).&lt;/li&gt;
&lt;li&gt;Use bucket policies with least privilege.&lt;/li&gt;
&lt;li&gt;Enable access logging to track who does what.&lt;/li&gt;
&lt;li&gt;Never put secrets in object keys or metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;aws s3 sync&lt;/code&gt; for uploading local folders; it only transfers changed files.&lt;/li&gt;
&lt;li&gt;For large files, use multipart uploads. The SDKs do this automatically for files over a threshold.&lt;/li&gt;
&lt;li&gt;When serving static websites, enable static website hosting on the bucket, but remember that requires public read access.&lt;/li&gt;
&lt;li&gt;Use S3 Select to query CSV/JSON files without downloading them, but it's rarely worth it unless you have huge files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;S3 is a workhorse. Once you understand the core concepts, you can build reliable and cost-effective storage. Start with a private bucket, use presigned URLs for uploads, and enable versioning. That covers most use cases. The rest is learning by doing.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloud</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
