<?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: Parag Agrawal</title>
    <description>The latest articles on DEV Community by Parag Agrawal (@parag477).</description>
    <link>https://dev.to/parag477</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F948885%2F0343e12c-b26b-428f-b3c8-5454b7628651.jpeg</url>
      <title>DEV Community: Parag Agrawal</title>
      <link>https://dev.to/parag477</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parag477"/>
    <language>en</language>
    <item>
      <title>Docker for Web Developers - The Only Guide You Actually Need (2026)</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Thu, 23 Apr 2026 07:58:17 +0000</pubDate>
      <link>https://dev.to/parag477/docker-for-web-developers-the-only-guide-you-actually-need-2026-4336</link>
      <guid>https://dev.to/parag477/docker-for-web-developers-the-only-guide-you-actually-need-2026-4336</guid>
      <description>&lt;p&gt;If you've ever said "it works on my machine," Docker is the fix.&lt;/p&gt;

&lt;p&gt;If you've ever spent hours debugging why your staging server behaves differently from your laptop, Docker is the fix.&lt;/p&gt;

&lt;p&gt;If you've ever deployed to a PaaS like Vercel or Railway and wondered "what's actually happening under the hood". Docker is what's happening.&lt;/p&gt;

&lt;p&gt;Yet the vast majority of Docker tutorials are written by and for DevOps engineers. They dive into container orchestration, overlay networks, and volume drivers before you've even containerized a "Hello World." That's backwards.&lt;/p&gt;

&lt;p&gt;This guide is different. It's written &lt;strong&gt;for web developers&lt;/strong&gt;, for the people building Next.js apps, Express APIs, Django backends, and Flask services. We'll cover exactly what you need to know, skip what you don't, and build up to production-ready skills in a single post.&lt;/p&gt;

&lt;p&gt;By the end, you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Containerize any web application&lt;/li&gt;
&lt;li&gt;✅ Write efficient, cache-optimized Dockerfiles&lt;/li&gt;
&lt;li&gt;✅ Use Docker Compose for local development with databases&lt;/li&gt;
&lt;li&gt;✅ Apply production best practices (security, size, speed)&lt;/li&gt;
&lt;li&gt;✅ Push images to a registry and deploy anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmztsxoe6ql9thgpmsfp8.png" alt="Docker Core Concepts" width="800" height="800"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 1: The Mental Model (2 Minutes)
&lt;/h2&gt;

&lt;p&gt;Before touching any commands, let's build the right mental model.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Docker Actually Does
&lt;/h3&gt;

&lt;p&gt;Docker packages your application, its dependencies, its runtime, and its configuration into a single, portable unit called a &lt;strong&gt;container&lt;/strong&gt;. That container runs identically everywhere may it be your laptop, your coworker's laptop, CI/CD, staging, production.&lt;/p&gt;

&lt;p&gt;Think of it like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without Docker&lt;/th&gt;
&lt;th&gt;With Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Install Node 20, then npm install, then set these env vars, then..."&lt;/td&gt;
&lt;td&gt;"Run &lt;code&gt;docker run my-app&lt;/code&gt;"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"It works on my machine"&lt;/td&gt;
&lt;td&gt;"It works on every machine"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different OS, different dependencies per environment&lt;/td&gt;
&lt;td&gt;Same environment everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We need to match the production Node version"&lt;/td&gt;
&lt;td&gt;Node version is locked in the Dockerfile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Four Core Concepts
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt; : A recipe (text file) that describes how to build your container image. Like a &lt;code&gt;package.json&lt;/code&gt; for your entire environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image&lt;/strong&gt; : The built result of a Dockerfile. A read-only snapshot containing your code, dependencies, runtime, and OS. Like a &lt;code&gt;.zip&lt;/code&gt; of your entire application stack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container&lt;/strong&gt; : A running instance of an image. You can run multiple containers from the same image. Like processes spawned from the same binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Registry&lt;/strong&gt; : A place to store and share images. Docker Hub is the public one. Amazon ECR is AWS's private one. Like npm for container images.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The flow:&lt;/strong&gt; You write a &lt;code&gt;Dockerfile&lt;/code&gt; → build it into an &lt;code&gt;Image&lt;/code&gt; → run the image as a &lt;code&gt;Container&lt;/code&gt; → push the image to a &lt;code&gt;Registry&lt;/code&gt; for deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Your First Dockerfile (5 Minutes)
&lt;/h2&gt;

&lt;p&gt;Let's containerize a real Node.js web application in under 5 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Application
&lt;/h3&gt;

&lt;p&gt;Here's a minimal Express API. Create a project folder with these files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt;&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-web-app"&lt;/span&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;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&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;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --watch server.js"&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;"dependencies"&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;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.21.0"&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;&lt;strong&gt;&lt;code&gt;server.js&lt;/code&gt;&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from Docker! 🐳&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Dockerfile
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;Dockerfile&lt;/code&gt; (no extension) in your project root:&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;# 1. Start from the official Node.js 20 Alpine image&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="c"&gt;# 2. Set the working directory inside the container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# 3. Copy dependency files first (for cache optimization)&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="c"&gt;# 4. Install dependencies&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="c"&gt;# 5. Copy everything else&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# 6. Tell Docker which port your app uses&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# 7. Define the command to run your app&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Seven lines. Let's break down what each line does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses Node.js 20 on Alpine Linux as the base&lt;/td&gt;
&lt;td&gt;Alpine is ~180MB vs ~1.1GB for the default image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WORKDIR /app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates &lt;code&gt;/app&lt;/code&gt; and sets it as the working directory&lt;/td&gt;
&lt;td&gt;Keeps things organized; avoids polluting the root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COPY package*.json ./&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copies &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Copied separately for layer caching (explained below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN npm ci --only=production&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs exact versions from lockfile, prod-only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm ci&lt;/code&gt; is faster and more reliable than &lt;code&gt;npm install&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COPY . .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copies the rest of your application code&lt;/td&gt;
&lt;td&gt;Done after &lt;code&gt;npm ci&lt;/code&gt; so code changes don't re-trigger install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Documents the port (doesn't actually open it)&lt;/td&gt;
&lt;td&gt;Informational; required by some platforms for auto-detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CMD ["node", "server.js"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the default command when the container starts&lt;/td&gt;
&lt;td&gt;Use exec form (JSON array) for proper signal handling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Build It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the image and tag it as "my-web-app"&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-web-app &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# [+] Building 12.3s (10/10) FINISHED&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; [1/5] FROM node:20-alpine&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; [2/5] WORKDIR /app&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; [3/5] COPY package*.json ./&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; [4/5] RUN npm ci --only=production&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; [5/5] COPY . .&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; naming to docker.io/library/my-web-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the container&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-web-app

&lt;span class="c"&gt;# -p 3000:3000 = map port 3000 on your machine to port 3000 in the container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; - your app is running inside a container. 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  Essential Run Variants
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run in the background (detached mode)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; my-app my-web-app

&lt;span class="c"&gt;# Run with environment variables&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret my-web-app

&lt;span class="c"&gt;# Run with a volume (live code changes, for development)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/app my-web-app npm run dev

&lt;span class="c"&gt;# See running containers&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# View logs&lt;/span&gt;
docker logs my-app

&lt;span class="c"&gt;# Stop and remove&lt;/span&gt;
docker stop my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 3: The .dockerignore File (Don't Skip This)
&lt;/h2&gt;

&lt;p&gt;Just like &lt;code&gt;.gitignore&lt;/code&gt; prevents files from entering your repo, &lt;code&gt;.dockerignore&lt;/code&gt; prevents files from entering your image. Without it, &lt;code&gt;COPY . .&lt;/code&gt; copies &lt;em&gt;everything&lt;/em&gt; including &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, test files, and local secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose.yml
.dockerignore
README.md
.vscode
.idea
coverage
tests
__tests__
*.test.js
*.spec.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;With .dockerignore&lt;/th&gt;
&lt;th&gt;Without .dockerignore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image: ~180 MB&lt;/td&gt;
&lt;td&gt;Image: ~400+ MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build time: ~12 sec&lt;/td&gt;
&lt;td&gt;Build time: ~30+ sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No secrets in image ✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; leaked into image ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faster CI/CD deploys&lt;/td&gt;
&lt;td&gt;Slower deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Critical:&lt;/strong&gt; Never include &lt;code&gt;.env&lt;/code&gt; files in your Docker image. Use environment variables passed at runtime (&lt;code&gt;docker run -e&lt;/code&gt;) or Docker secrets instead.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4: Understanding Layer Caching (The Key to Fast Builds)
&lt;/h2&gt;

&lt;p&gt;Docker builds images in &lt;strong&gt;layers&lt;/strong&gt;. Each instruction in your Dockerfile creates a layer. Docker caches these layers and reuses them when nothing has changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv339kax838k4ezewr4x0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv339kax838k4ezewr4x0.png" alt="Docker Layer Caching" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Order Matters
&lt;/h3&gt;

&lt;p&gt;Look at our Dockerfile again:&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 ./     # Layer 3: Changes rarely&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci                &lt;span class="c"&gt;# Layer 4: Changes rarely (cached if package.json unchanged)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                  # Layer 5: Changes on every code edit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we had done it the "obvious" way:&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;# ❌ BAD: Every code change re-runs npm install&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;npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you change a single line of code, Docker would re-install all dependencies from scratch. By copying &lt;code&gt;package.json&lt;/code&gt; first, Docker caches the &lt;code&gt;npm ci&lt;/code&gt; layer and only re-runs it when your dependencies change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The golden rule: Order Dockerfile instructions from least-frequently-changed to most-frequently-changed.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Time Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;With Cache Optimization&lt;/th&gt;
&lt;th&gt;Without&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First build&lt;/td&gt;
&lt;td&gt;45 seconds&lt;/td&gt;
&lt;td&gt;45 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code change (no dependency change)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3 seconds&lt;/strong&gt; ✅&lt;/td&gt;
&lt;td&gt;45 seconds ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency change&lt;/td&gt;
&lt;td&gt;40 seconds&lt;/td&gt;
&lt;td&gt;45 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Over 50 builds/day (common in active development), cache optimization saves &lt;strong&gt;~35 minutes daily&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Dockerfiles for Every Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Node.js / Express / Next.js
&lt;/h3&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: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;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="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; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python / Flask / Django
&lt;/h3&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-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="c"&gt;# Install dependencies first for caching&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;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="c"&gt;# Flask&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "-m", "flask", "run", "--host=0.0.0.0"]&lt;/span&gt;

&lt;span class="c"&gt;# Django (use this instead):&lt;/span&gt;
&lt;span class="c"&gt;# CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Go
&lt;/h3&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-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; 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; main .

&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:3.19&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 /app/main .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["./main"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Go example uses a &lt;strong&gt;multi-stage build&lt;/strong&gt; which we'll cover in detail in the next section. This is the pattern for compiled languages.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 6: Multi-Stage Builds (Smaller, Faster, Safer)
&lt;/h2&gt;

&lt;p&gt;Multi-stage builds let you use one Dockerfile with multiple &lt;code&gt;FROM&lt;/code&gt; instructions. The first stage builds your app; the final stage contains only the production output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Your build tools (TypeScript compiler, webpack, dev dependencies) don't need to be in your production image. Multi-stage builds strip them out automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Next.js Multi-Stage Build
&lt;/h3&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: Install dependencies&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;deps&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="c"&gt;# Stage 2: Build the application&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; --from=deps /app/node_modules ./node_modules&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;npm run build

&lt;span class="c"&gt;# Stage 3: Production image (only compiled output)&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;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Create non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 nodejs
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 nextjs

&lt;span class="c"&gt;# Copy only what's needed&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.next/standalone ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.next/static ./.next/static&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Size Impact
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Image Size&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single stage&lt;/td&gt;
&lt;td&gt;~800 MB&lt;/td&gt;
&lt;td&gt;Source + node_modules + devDeps + build output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-stage&lt;/td&gt;
&lt;td&gt;~180 MB&lt;/td&gt;
&lt;td&gt;Alpine + production node_modules + build output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-stage + standalone&lt;/td&gt;
&lt;td&gt;~120 MB&lt;/td&gt;
&lt;td&gt;Alpine + minimal Node.js + compiled output only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The multi-stage version is &lt;strong&gt;6x smaller&lt;/strong&gt; which means faster pulls, faster deploys, faster auto-scaling, and cheaper ECR storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: Choosing a Base Image
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmreg1har2uzstwnifrnv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmreg1har2uzstwnifrnv.png" alt="Docker Base Image Sizes Compared" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your choice of base image is the single biggest factor in final image size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js Base Images
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Trade-offs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1,100 MB&lt;/td&gt;
&lt;td&gt;Never use for production&lt;/td&gt;
&lt;td&gt;Full Debian; massive; includes compilers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~250 MB&lt;/td&gt;
&lt;td&gt;Good default for production&lt;/td&gt;
&lt;td&gt;Debian-slim; most native modules work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20-bookworm-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~220 MB&lt;/td&gt;
&lt;td&gt;Explicit Debian version&lt;/td&gt;
&lt;td&gt;Reproducible; predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~180 MB&lt;/td&gt;
&lt;td&gt;Best for most web apps&lt;/td&gt;
&lt;td&gt;Small; uses musl (rare compat issues with native modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/nodejs20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~130 MB&lt;/td&gt;
&lt;td&gt;Ultra-minimal production&lt;/td&gt;
&lt;td&gt;No shell, no package manager; hard to debug&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Python Base Images
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1,000 MB&lt;/td&gt;
&lt;td&gt;Development only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.12-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~150 MB&lt;/td&gt;
&lt;td&gt;Best default for production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.12-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~60 MB&lt;/td&gt;
&lt;td&gt;Smallest, but pip compilations can be slow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Our Recommendation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;-alpine&lt;/code&gt; for most web applications.&lt;/strong&gt; It's the best balance of size, security, and compatibility. If you hit issues with native modules (like &lt;code&gt;bcrypt&lt;/code&gt; or &lt;code&gt;sharp&lt;/code&gt;), switch to &lt;code&gt;-slim&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: Docker Compose (Local Development with Databases)
&lt;/h2&gt;

&lt;p&gt;Docker Compose lets you define and run multi-container applications. Instead of manually running your app, database, and cache as separate containers, you define them in a single file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Node.js + PostgreSQL + Redis
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;compose.yml&lt;/code&gt; both work)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=development&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://cache:6379&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;           &lt;span class="c1"&gt;# Live code reload&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt; &lt;span class="c1"&gt;# Don't override container's node_modules&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&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="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run Everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start all services&lt;/span&gt;
docker compose up

&lt;span class="c"&gt;# Start in background&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# View logs&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app

&lt;span class="c"&gt;# Stop everything&lt;/span&gt;
docker compose down

&lt;span class="c"&gt;# Stop and remove data (fresh database)&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What This Gives You
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One command&lt;/strong&gt; to spin up your entire development stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent database version&lt;/strong&gt; across all team members&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No local PostgreSQL/Redis installation&lt;/strong&gt; needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data persistence&lt;/strong&gt; via Docker volumes (survives &lt;code&gt;docker compose down&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic service discovery&lt;/strong&gt; : your app reaches Postgres at &lt;code&gt;db:5432&lt;/code&gt; and Redis at &lt;code&gt;cache:6379&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks&lt;/strong&gt; : the app waits for the database to be ready before starting&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Python Equivalent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DJANGO_SETTINGS_MODULE=myproject.settings&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python manage.py runserver 0.0.0.0:8000&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&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="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 9: Docker Compose Watch (Hot Reload in 2026)
&lt;/h2&gt;

&lt;p&gt;Docker Compose Watch (&lt;code&gt;docker compose watch&lt;/code&gt;) is the modern way to develop with Docker. Instead of brittle bind mounts, Compose Watch syncs file changes into the container automatically and can trigger rebuilds when needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;compose.yml&lt;/code&gt;&lt;/strong&gt; with Watch enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Sync code changes instantly (no rebuild)&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/src&lt;/span&gt;

        &lt;span class="c1"&gt;# Rebuild when dependencies change&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./package.json&lt;/span&gt;

        &lt;span class="c1"&gt;# Sync and restart when config changes&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync+restart&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./config&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start with watch mode&lt;/span&gt;
docker compose watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Watch is better than bind mounts:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Bind Mounts (&lt;code&gt;volumes&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Compose Watch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS performance&lt;/td&gt;
&lt;td&gt;Slow (file system translation)&lt;/td&gt;
&lt;td&gt;Fast (direct sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selective sync&lt;/td&gt;
&lt;td&gt;No (mounts entire directory)&lt;/td&gt;
&lt;td&gt;Yes (specify paths)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto rebuild on deps change&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;action: rebuild&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with Docker Build&lt;/td&gt;
&lt;td&gt;No (bypasses build)&lt;/td&gt;
&lt;td&gt;Yes (respects Dockerfile)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 10: Production Best Practices Checklist
&lt;/h2&gt;

&lt;p&gt;Before you deploy your Docker image to production (whether on ECS, Railway, or anywhere else), apply these practices:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Security
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Run as non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 appuser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="c"&gt;# 2. Don't store secrets in the image&lt;/span&gt;
&lt;span class="c"&gt;# ❌ BAD&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_KEY=sk-secret-123&lt;/span&gt;
&lt;span class="c"&gt;# ✅ GOOD - pass at runtime&lt;/span&gt;
&lt;span class="c"&gt;# docker run -e API_KEY=sk-secret-123 my-app&lt;/span&gt;

&lt;span class="c"&gt;# 3. Use specific image tags (not :latest)&lt;/span&gt;
&lt;span class="c"&gt;# ❌ BAD&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:latest&lt;/span&gt;
&lt;span class="c"&gt;# ✅ GOOD&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20.11.1-alpine3.19&lt;/span&gt;

&lt;span class="c"&gt;# 4. Add a health check&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s --start-period=5s --retries=3 \&lt;/span&gt;
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Size Optimization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Use multi-stage builds (covered above)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Combine RUN commands to reduce layers&lt;/span&gt;
&lt;span class="c"&gt;# ❌ BAD: 3 layers&lt;/span&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;span class="c"&gt;# ✅ GOOD: 1 layer&lt;/span&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;span class="c"&gt;# 3. Use --no-cache-dir for pip&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="c"&gt;# 4. Use npm ci instead of npm install&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Performance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Use .dockerignore (covered above)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Order layers from stable to volatile (covered above)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Pin versions for reproducibility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20.11.1-alpine3.19&lt;/span&gt;
&lt;span class="c"&gt;# NOT: FROM node:20-alpine (could change underneath you)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ The Complete Production Dockerfile (Node.js)
&lt;/h3&gt;

&lt;p&gt;Here's a production-ready Dockerfile that combines everything we've covered:&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;# syntax=docker/dockerfile:1&lt;/span&gt;

&lt;span class="c"&gt;# ----- Stage 1: Dependencies -----&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.11.1-alpine3.19&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;deps&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 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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# ----- Stage 2: 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.11.1-alpine3.19&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 package-lock.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 3: Production -----&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.11.1-alpine3.19&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;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 nodejs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 appuser

&lt;span class="c"&gt;# Copy production node_modules + built files&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps --chown=appuser:nodejs /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=appuser:nodejs /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=appuser:nodejs /app/package.json ./&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s --start-period=5s --retries=3 \&lt;/span&gt;
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 11: Essential Docker Commands Cheat Sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# === BUILD ===&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-app &lt;span class="nb"&gt;.&lt;/span&gt;                    &lt;span class="c"&gt;# Build an image&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-app:v1.2.3 &lt;span class="nb"&gt;.&lt;/span&gt;             &lt;span class="c"&gt;# Build with a specific tag&lt;/span&gt;
docker build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; my-app &lt;span class="nb"&gt;.&lt;/span&gt;         &lt;span class="c"&gt;# Build without cache&lt;/span&gt;
docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; my-app &lt;span class="nb"&gt;.&lt;/span&gt;  &lt;span class="c"&gt;# Build for x86 (on Apple Silicon)&lt;/span&gt;

&lt;span class="c"&gt;# === RUN ===&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-app              &lt;span class="c"&gt;# Run (foreground)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; app my-app  &lt;span class="c"&gt;# Run (background)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod my-app          &lt;span class="c"&gt;# Run with env var&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; my-app                      &lt;span class="c"&gt;# Auto-remove when stopped&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; app sh                      &lt;span class="c"&gt;# Shell into running container&lt;/span&gt;

&lt;span class="c"&gt;# === INSPECT ===&lt;/span&gt;
docker ps                                   &lt;span class="c"&gt;# List running containers&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;                                &lt;span class="c"&gt;# List all containers&lt;/span&gt;
docker images                               &lt;span class="c"&gt;# List images&lt;/span&gt;
docker logs app                             &lt;span class="c"&gt;# View logs&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; app                          &lt;span class="c"&gt;# Follow logs&lt;/span&gt;
docker stats                                &lt;span class="c"&gt;# Resource usage (CPU/memory)&lt;/span&gt;

&lt;span class="c"&gt;# === CLEANUP ===&lt;/span&gt;
docker stop app                             &lt;span class="c"&gt;# Stop a container&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;app                               &lt;span class="c"&gt;# Remove a container&lt;/span&gt;
docker rmi my-app                           &lt;span class="c"&gt;# Remove an image&lt;/span&gt;
docker system prune                         &lt;span class="c"&gt;# Remove all unused data&lt;/span&gt;
docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt;                      &lt;span class="c"&gt;# Remove everything unused (⚠️ aggressive)&lt;/span&gt;

&lt;span class="c"&gt;# === COMPOSE ===&lt;/span&gt;
docker compose up                           &lt;span class="c"&gt;# Start all services&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;                        &lt;span class="c"&gt;# Start in background&lt;/span&gt;
docker compose down                         &lt;span class="c"&gt;# Stop and remove&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;                      &lt;span class="c"&gt;# Stop + remove volumes (fresh DB)&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app                  &lt;span class="c"&gt;# Follow app logs&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;app sh                  &lt;span class="c"&gt;# Shell into a service&lt;/span&gt;
docker compose build                        &lt;span class="c"&gt;# Rebuild images&lt;/span&gt;
docker compose watch                        &lt;span class="c"&gt;# Start with file watching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 12: Pushing to a Registry (Deploy Anywhere)
&lt;/h2&gt;

&lt;p&gt;Once your image is built, you need to push it to a registry so deployment platforms can pull it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push to Docker Hub (Public)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Login&lt;/span&gt;
docker login

&lt;span class="c"&gt;# Tag&lt;/span&gt;
docker tag my-app yourusername/my-app:latest

&lt;span class="c"&gt;# Push&lt;/span&gt;
docker push yourusername/my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Push to Amazon ECR (Private - What TurboDeploy Uses)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Authenticate with ECR&lt;/span&gt;
aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 | &lt;span class="se"&gt;\&lt;/span&gt;
  docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt;your-account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com

&lt;span class="c"&gt;# Create repository (first time only)&lt;/span&gt;
aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; my-app

&lt;span class="c"&gt;# Tag&lt;/span&gt;
docker tag my-app:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt;your-account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/my-app:latest

&lt;span class="c"&gt;# Push&lt;/span&gt;
docker push &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt;your-account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once your image is in ECR, you can deploy it to &lt;a href="https://dev.to/parag477/aws-ecs-express-mode-getting-started-guide"&gt;ECS Express Mode&lt;/a&gt; in about 5 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Docker Fits in the TurboDeploy World
&lt;/h2&gt;

&lt;p&gt;Every container deployment platform like Vercel, Railway, Render, ECS and TurboDeploy runs Docker containers under the hood. Knowing Docker means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You understand what your PaaS is doing&lt;/strong&gt; : no more black box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can switch platforms without rewriting&lt;/strong&gt; : your Dockerfile works everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can optimize costs&lt;/strong&gt; : smaller images = faster deploys = lower bills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're ready for AWS&lt;/strong&gt; : ECS Fargate runs Docker containers natively&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With TurboDeploy, you write a Dockerfile, push to Git and we handle the rest. Building the image, pushing to ECR, deploying to ECS, provisioning the ALB and setting up monitoring. All in your AWS account, at AWS pricing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ Already comfortable with Docker? Check out &lt;a href="https://dev.to/parag477/deploy-docker-aws-ecs-fargate"&gt;How to Deploy a Docker Container on AWS ECS Fargate&lt;/a&gt; our step-by-step deployment guide.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What You Need to Know&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recipe for building your image. Order matters for caching.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;-alpine&lt;/code&gt; or &lt;code&gt;-slim&lt;/code&gt;. Never &lt;code&gt;:latest&lt;/code&gt; in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;.dockerignore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Always create one. Never ship &lt;code&gt;node_modules&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layer caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy dependencies before code. Saves 80%+ build time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-stage builds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use for compiled/built apps. Reduces image size by 4–6x.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use for local dev with databases. One command, full stack.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compose Watch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Replace bind mounts for development. Faster on macOS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Non-root user, specific tags, no secrets in image, health checks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Registries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker Hub (public), ECR (private). Push before deploy.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Skip the Docker learning curve entirely?&lt;/strong&gt; &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;TurboDeploy&lt;/a&gt; detects your framework, generates an optimized Dockerfile,and deploys to your AWS account with no Docker knowledge required. But if you want to understand the engine under the hood, this guide has you covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;Join the waitlist →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>node</category>
    </item>
    <item>
      <title>The "Platform Tax" Is Real - Why Developers Are Moving Back to AWS (2026)</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Tue, 21 Apr 2026 12:56:25 +0000</pubDate>
      <link>https://dev.to/parag477/the-platform-tax-is-real-why-developers-are-moving-back-to-aws-2026-1com</link>
      <guid>https://dev.to/parag477/the-platform-tax-is-real-why-developers-are-moving-back-to-aws-2026-1com</guid>
      <description>&lt;p&gt;There's a phrase we keep hearing in founder Slack groups, Reddit threads, and Twitter conversations about infrastructure costs. It comes up when someone posts their monthly Vercel bill, or when a seed-stage startup realizes they're spending $400/month on Railway for a workload that would cost $120 on AWS.&lt;/p&gt;

&lt;p&gt;The phrase is: &lt;strong&gt;"The Platform Tax."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's not an official term. No pricing page calls it that. But every developer who's outgrown a PaaS platform knows exactly what it means — the premium you pay, on top of actual infrastructure costs, for the convenience of not dealing with AWS directly.&lt;/p&gt;

&lt;p&gt;And in 2026, a growing number of developers are deciding that tax isn't worth it anymore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8tymwqlosvj6nvcjt06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8tymwqlosvj6nvcjt06.png" alt="The Platform Tax Explained" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Exactly Is the "Platform Tax"?
&lt;/h2&gt;

&lt;p&gt;The platform tax is the delta between what you pay a PaaS provider and what the same workload costs on the underlying cloud infrastructure.&lt;/p&gt;

&lt;p&gt;Let's make it concrete. In our &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;real-world pricing breakdown&lt;/a&gt;, we compared identical workloads across Vercel, Railway, Render, and AWS ECS Fargate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload (Growth Tier: 3 services, 1 vCPU each)&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Platform Tax&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AWS ECS Fargate&lt;/strong&gt; (direct)&lt;/td&gt;
&lt;td&gt;~$125&lt;/td&gt;
&lt;td&gt;$0 (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$140&lt;/td&gt;
&lt;td&gt;~$15/mo (12%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render Professional&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$165&lt;/td&gt;
&lt;td&gt;~$40/mo (32%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$175&lt;/td&gt;
&lt;td&gt;~$50/mo (40%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale (8 services, 1.5 vCPU each):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload (Scale Tier)&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Platform Tax&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AWS ECS Fargate&lt;/strong&gt; (direct)&lt;/td&gt;
&lt;td&gt;~$390&lt;/td&gt;
&lt;td&gt;$0 (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$430&lt;/td&gt;
&lt;td&gt;~$40/mo (10%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render Organization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$480&lt;/td&gt;
&lt;td&gt;~$90/mo (23%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$620&lt;/td&gt;
&lt;td&gt;~$230/mo (59%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The platform tax is real. It's measurable. And it compounds over time.&lt;/p&gt;

&lt;p&gt;Over 12 months at the Scale tier, the difference between Vercel and direct AWS is &lt;strong&gt;~$2,760&lt;/strong&gt; — enough to fund a contractor, extend runway, or invest in product features. And that's &lt;em&gt;before&lt;/em&gt; AWS optimization strategies like Savings Plans or Graviton ARM processors, which can widen the gap to 40–65%.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Isn't Just About Money
&lt;/h2&gt;

&lt;p&gt;Here's the thing people miss when they frame PaaS-vs-AWS as purely a cost decision. The platform tax has three components:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Financial Tax (Direct Markup)
&lt;/h3&gt;

&lt;p&gt;This is the straightforward part — you're paying more per vCPU, per GB of memory, and per GB of bandwidth than the underlying cloud charges. We covered this in detail in &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Lock-In Tax (Switching Costs)
&lt;/h3&gt;

&lt;p&gt;Every month you stay on a PaaS platform, your switching costs increase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your deployment configs are in &lt;code&gt;vercel.json&lt;/code&gt;, &lt;code&gt;railway.toml&lt;/code&gt;, or &lt;code&gt;render.yaml&lt;/code&gt; — none of which work anywhere else&lt;/li&gt;
&lt;li&gt;Your team builds muscle memory around the platform's CLI and dashboard&lt;/li&gt;
&lt;li&gt;Your environment variables, secrets, and configurations live in their UI&lt;/li&gt;
&lt;li&gt;If you're using their managed databases, migration means downtime and risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lock-in tax isn't on your monthly bill, but it's on your balance sheet. It's the engineering time you'll eventually spend migrating — and the longer you wait, the more expensive it gets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated lock-in migration cost:&lt;/strong&gt; 1–4 weeks of senior engineer time = &lt;strong&gt;$5,000–$20,000+&lt;/strong&gt; in opportunity cost, depending on complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Ceiling Tax (Capability Limits)
&lt;/h3&gt;

&lt;p&gt;PaaS platforms are designed for the 80% use case. They work brilliantly for the first 80% of your journey. But eventually, you run into limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need SOC 2 compliance? Enterprise tier, custom pricing&lt;/li&gt;
&lt;li&gt;Need SAML SSO? Enterprise tier&lt;/li&gt;
&lt;li&gt;Need HIPAA compliance? "Contact us"&lt;/li&gt;
&lt;li&gt;Need multi-region deployment? Not available on most PaaS&lt;/li&gt;
&lt;li&gt;Need VPC peering for a managed database? Not supported or limited&lt;/li&gt;
&lt;li&gt;Need custom networking for security? You can't customize what you don't own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time you hit one of these ceilings, you face a choice: pay the enterprise upgrade tax, or migrate to infrastructure you control. And by then, the lock-in tax makes migration painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Timeline: How We Got Here
&lt;/h2&gt;

&lt;p&gt;The developer community's relationship with PaaS has evolved significantly over the past six years.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay47eatnpxizscqv1k89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay47eatnpxizscqv1k89.png" alt="The Great PaaS-to-AWS Migration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2020–2021: The Golden Age of PaaS
&lt;/h3&gt;

&lt;p&gt;Heroku's free tier was running half the startup ecosystem. Vercel launched and made Next.js deployment magic. Railway emerged as the hip Heroku alternative. Render promised to be "the modern Heroku."&lt;/p&gt;

&lt;p&gt;Developer experience was the #1 priority. Nobody cared about costs because usage was small and VC money was flowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2022: The Wake-Up Call
&lt;/h3&gt;

&lt;p&gt;Two things happened simultaneously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Heroku killed its free tier&lt;/strong&gt; (November 2022). Millions of developers were suddenly paying for something that used to be free. Many migrated to Railway and Render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interest rates rose.&lt;/strong&gt; VC funding tightened. Startups started caring about unit economics and burn rate for the first time in years.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Developers who moved from Heroku to Railway/Render solved the immediate problem but didn't question the underlying model: someone else owns your infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2023–2024: The Repatriation Wave Begins
&lt;/h3&gt;

&lt;p&gt;The biggest signal came from DHH and 37signals (makers of Basecamp and HEY):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They famously &lt;strong&gt;left the cloud entirely&lt;/strong&gt; in 2023–2024&lt;/li&gt;
&lt;li&gt;They purchased their own servers and moved off AWS&lt;/li&gt;
&lt;li&gt;They reported &lt;strong&gt;saving over $1 million per year&lt;/strong&gt; in cloud costs&lt;/li&gt;
&lt;li&gt;DHH wrote extensively about "cloud repatriation" — the trend of companies bringing workloads back from managed services to owned infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Was their approach right for everyone? No — most startups don't need to buy physical servers. But the &lt;em&gt;sentiment&lt;/em&gt; it unleashed was massive. Developers started asking: "Wait, how much am I actually paying in platform markup?"&lt;/p&gt;

&lt;h3&gt;
  
  
  2025: The Self-Hosted PaaS Boom
&lt;/h3&gt;

&lt;p&gt;Open-source self-hosted PaaS tools exploded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coolify&lt;/strong&gt; — self-hosted Vercel/Netlify/Heroku alternative (26K+ GitHub stars)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dokku&lt;/strong&gt; — the original "mini-Heroku" (29K+ GitHub stars)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kamal&lt;/strong&gt; — DHH's own Docker deployment tool (11K+ GitHub stars)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CapRover&lt;/strong&gt; — another self-hosted PaaS option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The message was clear: developers wanted PaaS &lt;em&gt;simplicity&lt;/em&gt; but didn't want to pay the platform tax. They'd rather run deployment tools on their own servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2026: AWS Responds
&lt;/h3&gt;

&lt;p&gt;AWS caught the signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECS Express Mode&lt;/strong&gt; launched — making ECS nearly as simple as App Runner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Runner was sunset&lt;/strong&gt; — AWS consolidated into one simplified path&lt;/li&gt;
&lt;li&gt;Docker's tooling matured — multi-stage builds, compose watch, buildx&lt;/li&gt;
&lt;li&gt;GitHub Actions reached maturity — free CI/CD pipelines removed another PaaS advantage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between "PaaS simple" and "AWS simple" is the narrowest it's ever been. And the cost gap remains enormous.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Five Signs You've Outgrown Your PaaS
&lt;/h2&gt;

&lt;p&gt;How do you know when the platform tax is no longer worth paying? Here are the signals:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your Monthly Bill Has a Significant Platform Component
&lt;/h3&gt;

&lt;p&gt;If your PaaS bill is 30%+ higher than equivalent AWS pricing, the convenience premium is getting expensive. At $50+/month in platform tax, that's $600/year you could put toward product.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. You're Working Around Platform Limitations
&lt;/h3&gt;

&lt;p&gt;If you find yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Splitting one service into two because of memory limits&lt;/li&gt;
&lt;li&gt;Caching aggressively to avoid bandwidth overages&lt;/li&gt;
&lt;li&gt;Avoiding certain architectures because your PaaS doesn't support them&lt;/li&gt;
&lt;li&gt;Running some services on the PaaS and others on AWS already&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...you've outgrown the platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Security or Compliance Comes Calling
&lt;/h3&gt;

&lt;p&gt;The moment a customer asks "Where is our data hosted?" or "Can we see your SOC 2 report?", PaaS platforms become a liability. You can't answer those questions definitively when you don't own the infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Your Team Has Grown Beyond 3 Engineers
&lt;/h3&gt;

&lt;p&gt;Per-seat pricing ($20–29/user/month) becomes a real line item. With 5 developers, you're paying $100–145/month just for the &lt;em&gt;right to deploy&lt;/em&gt; — before any compute costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. You're Thinking About Fundraising
&lt;/h3&gt;

&lt;p&gt;Investors increasingly ask about infrastructure costs and margins. "We're paying 40% more than we need to because we're on [PaaS]" is not a great answer. "We run on AWS in our own account at direct rates" signals maturity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Infrastructure Ownership Spectrum
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qb6e8cxictlxwv6d6p3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qb6e8cxictlxwv6d6p3.png" alt="The Infrastructure Ownership Spectrum" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real question isn't "PaaS vs AWS." It's where you want to sit on the ownership spectrum:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Full PaaS&lt;/strong&gt; (Vercel, Railway, Render)&lt;/td&gt;
&lt;td&gt;❌ Low&lt;/td&gt;
&lt;td&gt;✅ Low&lt;/td&gt;
&lt;td&gt;❌ Highest&lt;/td&gt;
&lt;td&gt;MVPs, side projects, tiny teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Bring Your Own Cloud&lt;/strong&gt; (TurboDeploy, Coolify)&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;✅ Low&lt;/td&gt;
&lt;td&gt;✅ Low (AWS direct)&lt;/td&gt;
&lt;td&gt;Growth-stage startups, cost-conscious teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Full DIY AWS&lt;/strong&gt; (Terraform, CDK, manual ECS)&lt;/td&gt;
&lt;td&gt;✅ Highest&lt;/td&gt;
&lt;td&gt;❌ Highest&lt;/td&gt;
&lt;td&gt;✅ Lowest (with expertise)&lt;/td&gt;
&lt;td&gt;Mature teams with dedicated DevOps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap in the middle — &lt;strong&gt;"I want to own my infrastructure but I don't want to learn 47 AWS services"&lt;/strong&gt; — is exactly where the industry is moving. And it's exactly where we're building &lt;a href="https://dev.to/parag477/why-we-are-building-turbodeploy"&gt;TurboDeploy&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Community Is Saying
&lt;/h2&gt;

&lt;p&gt;We're not the only ones noticing this trend. Across developer communities, the sentiment is shifting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Reddit r/devops and r/aws:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Started on Railway, loved it, then realized I was paying 3x what the same Fargate setup costs. Migrated in a weekend."&lt;/p&gt;

&lt;p&gt;"Vercel is great for frontend. For backend? Just use ECS. The DX gap isn't big enough anymore to justify the cost."&lt;/p&gt;

&lt;p&gt;"The move from 'I don't want to deal with AWS' to 'actually, AWS isn't that bad' happened faster than I expected."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;On Twitter/X (paraphrased, common sentiment):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hot take: managed PaaS is the new 'premium IDE license' — nice to have when you're not paying, painful when you scale."&lt;/p&gt;

&lt;p&gt;"Every dollar I spend on Render that I don't need to is a dollar I can't spend on marketing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;On HackerNews (a recurring theme):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The PaaS value proposition was always 'pay us more so you don't have to learn AWS.' But now ECS is simple enough that the premium isn't justified."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pattern is consistent: developers who &lt;em&gt;start&lt;/em&gt; on PaaS eventually &lt;em&gt;leave&lt;/em&gt; PaaS. The only question is when — and how painful the migration will be.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Counterargument (And Why It's Partially Valid)
&lt;/h2&gt;

&lt;p&gt;Let's be honest about the other side.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Developer time is more expensive than PaaS markup"
&lt;/h3&gt;

&lt;p&gt;This is true — &lt;em&gt;at small scale&lt;/em&gt;. If you're a solo founder and your total infra cost is $50/month, the $20/month platform tax is genuinely worth it if it saves you 4 hours of AWS setup. Your time &lt;em&gt;is&lt;/em&gt; more valuable.&lt;/p&gt;

&lt;p&gt;The math stops working when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform tax exceeds ~$100/month&lt;/li&gt;
&lt;li&gt;You have team members paying per-seat fees&lt;/li&gt;
&lt;li&gt;The time savings decrease (ECS Express Mode, GitHub Actions, etc. have closed the DX gap)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  "PaaS platforms provide more than just compute"
&lt;/h3&gt;

&lt;p&gt;True. They provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preview environments&lt;/li&gt;
&lt;li&gt;Rollback with one click&lt;/li&gt;
&lt;li&gt;Built-in CI/CD&lt;/li&gt;
&lt;li&gt;Domain management&lt;/li&gt;
&lt;li&gt;Team access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But these aren't unique to PaaS anymore. GitHub Actions provides CI/CD. ECS provides rollback. Route 53 handles domains. IAM handles access control. The feature gap has narrowed dramatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Migration is risky"
&lt;/h3&gt;

&lt;p&gt;Also true. Any migration carries risk. But the risk &lt;em&gt;increases&lt;/em&gt; the longer you wait:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More services = more to migrate&lt;/li&gt;
&lt;li&gt;More data = longer downtime&lt;/li&gt;
&lt;li&gt;More team knowledge = harder to relearn&lt;/li&gt;
&lt;li&gt;Longer lock-in = higher switching costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're going to migrate eventually (and most scaling startups do), doing it earlier is cheaper.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Third Path: Don't Choose
&lt;/h2&gt;

&lt;p&gt;Here's our thesis at TurboDeploy: the "PaaS vs AWS" framing is a false dichotomy.&lt;/p&gt;

&lt;p&gt;You shouldn't have to choose between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple but expensive&lt;/strong&gt; (PaaS) and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap but painful&lt;/strong&gt; (raw AWS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third path is &lt;strong&gt;simple AND cheap AND yours&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy with a &lt;code&gt;git push&lt;/code&gt; — no AWS console required ✅&lt;/li&gt;
&lt;li&gt;Runs in YOUR AWS account — you own everything ✅&lt;/li&gt;
&lt;li&gt;Pay AWS pricing — no platform markup ✅&lt;/li&gt;
&lt;li&gt;Full access to underlying resources — no black box ✅&lt;/li&gt;
&lt;li&gt;No vendor lock-in — your infra survives if TurboDeploy disappeared tomorrow ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what we're building. And based on the trend lines we've outlined in this post — the DHH repatriation wave, the self-hosted PaaS boom, ECS Express Mode, App Runner's sunset — we believe this "Bring Your Own Cloud" model is where the entire market is heading.&lt;/p&gt;

&lt;p&gt;The platform tax had its era. That era is ending.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ Read more about our approach: &lt;a href="https://dev.to/parag477/why-we-are-building-turbodeploy"&gt;Why We're Building TurboDeploy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Items: What You Should Do Now
&lt;/h2&gt;

&lt;p&gt;Whether or not you use TurboDeploy, here's what we'd recommend based on where you are:&lt;/p&gt;

&lt;h3&gt;
  
  
  🟢 If You're Happy on PaaS (&amp;lt; $50/mo total)
&lt;/h3&gt;

&lt;p&gt;Stay. The platform tax at this scale is genuinely worth it. Focus on building product.&lt;/p&gt;

&lt;p&gt;But: &lt;strong&gt;Know your numbers.&lt;/strong&gt; Run the math from our &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;pricing breakdown&lt;/a&gt; quarterly. Set a threshold (e.g., "when we hit $200/mo, we migrate") and stick to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  🟡 If You're Feeling the Squeeze ($100–300/mo PaaS bill)
&lt;/h3&gt;

&lt;p&gt;Start planning. You're in the zone where the platform tax is eating real money — $30–100/month — without proportional value.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your bill.&lt;/strong&gt; Break down compute, bandwidth, build minutes, per-seat costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculate the AWS equivalent.&lt;/strong&gt; Use our &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown#tier-2-growth--the-early-startup"&gt;growth-tier calculator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try ECS Express Mode&lt;/strong&gt; on a non-critical service. See our &lt;a href="https://dev.to/parag477/aws-ecs-express-mode-getting-started-guide"&gt;complete guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a migration date.&lt;/strong&gt; Even tentatively — having a deadline prevents drift&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🔴 If You're Paying &amp;gt; $300/mo in Platform Tax
&lt;/h3&gt;

&lt;p&gt;Stop reading and start migrating. At $300+/month in platform tax, you're spending &lt;strong&gt;$3,600+/year&lt;/strong&gt; on convenience that ECS Express Mode or TurboDeploy can replicate for free.&lt;/p&gt;

&lt;p&gt;The longer you wait, the more expensive migration becomes. The code isn't getting simpler, the data isn't getting smaller, and the lock-in isn't loosening.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The Platform Tax...&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The markup PaaS platforms charge above actual cloud infrastructure costs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How much it costs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10–59% depending on platform and scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Three components&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Financial (direct markup) + Lock-in (switching costs) + Ceiling (capability limits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why it's growing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-seat pricing, bandwidth overages, enterprise feature gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why developers are leaving&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DHH effect, self-hosted PaaS boom, ECS Express Mode, tighter startup budgets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The alternative&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Bring Your Own Cloud" — PaaS simplicity, AWS pricing, your infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The platform tax was an acceptable cost when AWS was impossibly complex. In 2026, that's no longer the case.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;Join the TurboDeploy waitlist →&lt;/a&gt;&lt;/strong&gt; — Deploy to AWS without the AWS headache. Your code, your AWS, no platform tax.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>startup</category>
    </item>
    <item>
      <title>AWS ECS Express Mode: The Complete Getting Started Guide (2026)</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Fri, 17 Apr 2026 09:24:29 +0000</pubDate>
      <link>https://dev.to/parag477/aws-ecs-express-mode-the-complete-getting-started-guide-2026-257j</link>
      <guid>https://dev.to/parag477/aws-ecs-express-mode-the-complete-getting-started-guide-2026-257j</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/parag477/aws-app-runner-dead-what-to-use-instead"&gt;last post&lt;/a&gt;, we broke the news: AWS App Runner is sunsetting. No new customers after April 30, 2026. If you were one of the developers who loved App Runner's simplicity, you probably felt a pang of dread. Where do you go now?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;ECS Express Mode&lt;/strong&gt; and after using it, we think it's actually &lt;em&gt;better&lt;/em&gt; than App Runner ever was.&lt;/p&gt;

&lt;p&gt;This guide walks you through deploying your first containerized web application with ECS Express Mode from scratch. By the end, you'll have a running service with an auto-provisioned load balancer, auto-scaling, HTTPS, and monitoring — all in under 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq7o3m4k3qd2gtfr5p3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq7o3m4k3qd2gtfr5p3j.png" alt="ECS Express Mode Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is ECS Express Mode?
&lt;/h2&gt;

&lt;p&gt;ECS Express Mode is a new capability within Amazon ECS (Elastic Container Service) that dramatically simplifies container deployment. Think of it as the &lt;strong&gt;spiritual successor to AWS App Runner&lt;/strong&gt; — but built directly into ECS, giving you both simplicity &lt;em&gt;and&lt;/em&gt; the full power of the ECS ecosystem.&lt;/p&gt;

&lt;p&gt;Here's what Express Mode does automatically when you deploy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What It Provisions&lt;/th&gt;
&lt;th&gt;What You'd Otherwise Need to Do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECS Cluster&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create a cluster with Fargate capacity providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECS Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Define service configuration, deployment strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application Load Balancer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create ALB, listener, target group, health check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Groups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configure inbound/outbound rules for ALB and tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Set up Application Auto Scaling targets and policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudWatch Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configure log groups, metrics, alarms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Set up VPC subnets, route tables, internet gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;6–8 AWS console pages&lt;/strong&gt; reduced to a single form. This is not a wrapper with limited features — Express Mode creates real, standard ECS resources that you can inspect, modify, and extend at any time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Differentiator: Shared ALBs
&lt;/h3&gt;

&lt;p&gt;One of the most impactful features is &lt;strong&gt;shared Application Load Balancers&lt;/strong&gt;. In standard ECS, every service typically needs its own ALB — that's ~$16/month per service, just for the load balancer.&lt;/p&gt;

&lt;p&gt;ECS Express Mode can share a single ALB across &lt;strong&gt;up to 25 services&lt;/strong&gt; using path-based or host-based routing. For a startup running 5 services, that's a savings of ~$64/month just in ALB costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  ECS Express Mode vs Standard ECS: When to Use What
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgovld1rz8o8nbbcmjb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgovld1rz8o8nbbcmjb0.png" alt="ECS Express Mode vs Standard Mode" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Express Mode&lt;/th&gt;
&lt;th&gt;Standard ECS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5–10 minutes&lt;/td&gt;
&lt;td&gt;30–60 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ALB provisioning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic (shared)&lt;/td&gt;
&lt;td&gt;Manual (dedicated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security groups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auto-configured&lt;/td&gt;
&lt;td&gt;Manual configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-configured policies&lt;/td&gt;
&lt;td&gt;Manual setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudWatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auto-enabled&lt;/td&gt;
&lt;td&gt;Manual log groups/metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC networking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses defaults or existing&lt;/td&gt;
&lt;td&gt;Full manual control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customizability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate (can modify after creation)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same Fargate + ALB pricing&lt;/td&gt;
&lt;td&gt;Same Fargate + ALB pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most web services, APIs, backends&lt;/td&gt;
&lt;td&gt;Custom networking, multi-AZ, advanced configs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Our recommendation:&lt;/strong&gt; Start with Express Mode for everything. If you hit a limitation, you can always "eject" to standard ECS by modifying the underlying resources directly. You're not locked in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. An AWS Account
&lt;/h3&gt;

&lt;p&gt;If you don't have one, &lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;create a free account&lt;/a&gt;. New accounts get 12 months of free tier, which includes enough ECS/Fargate credits to run this tutorial at zero cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AWS CLI Installed (Optional, for CLI Path)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;awscli

&lt;span class="c"&gt;# Verify&lt;/span&gt;
aws &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# aws-cli/2.x.x ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Docker Installed (for Building Images)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; docker

&lt;span class="c"&gt;# Verify&lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Docker version 27.x.x ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. A Container Image
&lt;/h3&gt;

&lt;p&gt;You need a Docker image to deploy. You can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a public image&lt;/strong&gt; for testing (we'll use &lt;code&gt;public.ecr.aws/docker/library/nginx:latest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build your own&lt;/strong&gt; from your app's Dockerfile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this guide, we'll show both paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deployment Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6q8ybhdvmo090jtn1ci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6q8ybhdvmo090jtn1ci.png" alt="ECS Express Mode 5-Step Deployment Flow" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the complete path from code to running service:&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Prepare Your Container Image
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option A: Use a Public Image (Quickest)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip ahead to Step 2 and use &lt;code&gt;public.ecr.aws/docker/library/nginx:latest&lt;/code&gt; as your image URI. This is the fastest way to test Express Mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: Build and Push Your Own Image&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say you have a simple Node.js API. First, create a &lt;code&gt;Dockerfile&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="c"&gt;# Use official Node.js LTS image&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="c"&gt;# Set working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy package files&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="c"&gt;# Install dependencies (production only)&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="c"&gt;# Copy application code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Expose the port your app runs on&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# Health check (Express Mode uses this)&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s \&lt;/span&gt;
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

&lt;span class="c"&gt;# Start the application&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now build and push to Amazon ECR:&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;# Set your variables&lt;/span&gt;
&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nv"&gt;AWS_ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Account &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REPO_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-web-app

&lt;span class="c"&gt;# Create ECR repository (if it doesn't exist)&lt;/span&gt;
aws ecr create-repository &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; &lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-scanning-configuration&lt;/span&gt; &lt;span class="nv"&gt;scanOnPush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Authenticate Docker with ECR&lt;/span&gt;
aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$AWS_ACCOUNT_ID&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;.amazonaws.com

&lt;span class="c"&gt;# Build the image&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Tag the image&lt;/span&gt;
docker tag &lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt;:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$AWS_ACCOUNT_ID&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;.amazonaws.com/&lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt;:latest

&lt;span class="c"&gt;# Push to ECR&lt;/span&gt;
docker push &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$AWS_ACCOUNT_ID&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;.amazonaws.com/&lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt;:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; If you're on an Apple Silicon Mac (M1/M2/M3), add &lt;code&gt;--platform linux/amd64&lt;/code&gt; to your &lt;code&gt;docker build&lt;/code&gt; command to ensure compatibility with Fargate's x86 environment. Or use &lt;code&gt;linux/arm64&lt;/code&gt; and select ARM/Graviton in Express Mode for 20% cost savings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Save the full image URI — you'll need it in the next step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;your-account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Open the ECS Console and Select Express Mode
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;a href="https://console.aws.amazon.com/ecs/v2" rel="noopener noreferrer"&gt;Amazon ECS Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;In the left navigation panel, click &lt;strong&gt;Express mode&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll see a simplified form — much less intimidating than the standard ECS service creation page. This is by design.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Configure Your Service
&lt;/h3&gt;

&lt;p&gt;Fill in the Express Mode creation form:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;my-web-app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase, hyphens allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container image URI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your ECR URI or a public image&lt;/td&gt;
&lt;td&gt;e.g., &lt;code&gt;public.ecr.aws/docker/library/nginx:latest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;3000&lt;/code&gt; (or &lt;code&gt;80&lt;/code&gt; for nginx)&lt;/td&gt;
&lt;td&gt;The port your app listens on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.25 vCPU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start small; you can scale later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5 GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start with 512 MB for most web apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Environment Variables (optional):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click "Add environment variable" to add any your app needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NODE_ENV=production
DATABASE_URL=your-connection-string
API_KEY=your-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Security note:&lt;/strong&gt; For sensitive values like database credentials and API keys, use &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; or &lt;strong&gt;SSM Parameter Store&lt;/strong&gt; instead of plain-text environment variables. Express Mode supports both through the "valueFrom" syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scaling Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Desired count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start with 1 task for testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Min tasks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum for auto-scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max tasks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum for auto-scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Step 4: Create and Wait
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Create&lt;/strong&gt;. Express Mode will now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Create an ECS cluster (if one doesn't exist)&lt;/li&gt;
&lt;li&gt;✅ Register a task definition with your image and configuration&lt;/li&gt;
&lt;li&gt;✅ Provision (or reuse) an Application Load Balancer&lt;/li&gt;
&lt;li&gt;✅ Create a target group and listener rules&lt;/li&gt;
&lt;li&gt;✅ Configure security groups for both ALB and tasks&lt;/li&gt;
&lt;li&gt;✅ Set up CloudWatch log group and Container Insights&lt;/li&gt;
&lt;li&gt;✅ Launch your Fargate task(s)&lt;/li&gt;
&lt;li&gt;✅ Configure auto-scaling policies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This takes about &lt;strong&gt;5–6 minutes&lt;/strong&gt;. You'll see the status go from "Provisioning" → "Running."&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5: Access Your Application
&lt;/h3&gt;

&lt;p&gt;Once the status shows &lt;strong&gt;Running&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the ECS console, click on your service&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Networking&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;Load balancer DNS name&lt;/strong&gt; — it'll look like:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   my-web-app-alb-123456789.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open that URL in your browser — your app is live! 🎉&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;To add a custom domain:&lt;/strong&gt; Create a CNAME record in your DNS provider pointing your domain to the ALB DNS name. For HTTPS, attach an AWS Certificate Manager (ACM) certificate to the ALB listener.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Just Happened Under the Hood?
&lt;/h2&gt;

&lt;p&gt;Express Mode created real, standard AWS resources. Let's peek behind the curtain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your AWS Account
├── ECS Cluster: "default" (or new)
│   └── Service: "my-web-app"
│       └── Task: running on Fargate
│           └── Container: your image
├── Application Load Balancer (shared)
│   ├── Listener (port 80/443)
│   └── Target Group → your tasks
├── Security Groups
│   ├── ALB SG (allows 80/443 inbound)
│   └── Task SG (allows traffic from ALB SG only)
├── CloudWatch
│   ├── Log Group: /ecs/my-web-app
│   └── Container Insights: enabled
├── IAM Roles
│   ├── Task Execution Role (pulls images, writes logs)
│   └── Task Role (your app's AWS permissions)
└── Auto Scaling
    ├── Target: ECS service
    └── Policy: target tracking (CPU/memory-based)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Every one of these resources is visible in your AWS console.&lt;/strong&gt; You can modify, extend, or even detach them from Express Mode if you need full control. This is the fundamental difference from App Runner — you're not using a black box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying Updates
&lt;/h2&gt;

&lt;p&gt;After your initial deployment, updating your app is straightforward:&lt;/p&gt;

&lt;h3&gt;
  
  
  Via Console
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Build and push a new image to ECR with a new tag (or &lt;code&gt;:latest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Go to your ECS service in the console&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Update&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the new task definition revision (or force a new deployment if using &lt;code&gt;:latest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;ECS performs a &lt;strong&gt;rolling update&lt;/strong&gt; — no downtime&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Via CLI (Faster)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Force a new deployment (re-pulls :latest image)&lt;/span&gt;
aws ecs update-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service&lt;/span&gt; my-web-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Via GitHub Actions (Best for Production)
&lt;/h3&gt;

&lt;p&gt;Here's a minimal GitHub Actions workflow that deploys on every push to &lt;code&gt;main&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;Deploy to ECS Express&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;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;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;Configure AWS credentials&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;aws-actions/configure-aws-credentials@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;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-deploy&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&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;Login to Amazon ECR&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login-ecr&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;aws-actions/amazon-ecr-login@v2&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, tag, and push image&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ECR_REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.registry }}&lt;/span&gt;
          &lt;span class="na"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&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;docker build -t $ECR_REGISTRY/my-web-app:$IMAGE_TAG .&lt;/span&gt;
          &lt;span class="s"&gt;docker push $ECR_REGISTRY/my-web-app:$IMAGE_TAG&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;Update ECS service&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;aws ecs update-service \&lt;/span&gt;
            &lt;span class="s"&gt;--cluster default \&lt;/span&gt;
            &lt;span class="s"&gt;--service my-web-app \&lt;/span&gt;
            &lt;span class="s"&gt;--force-new-deployment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Pro tip:&lt;/strong&gt; Use OIDC (OpenID Connect) instead of storing AWS access keys in GitHub Secrets. It's more secure and is the &lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services" rel="noopener noreferrer"&gt;recommended approach in 2026&lt;/a&gt;. The &lt;code&gt;role-to-assume&lt;/code&gt; parameter above uses OIDC.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Cost Breakdown: What You'll Actually Pay
&lt;/h2&gt;

&lt;p&gt;ECS Express Mode doesn't add any surcharge — you pay standard AWS pricing:&lt;/p&gt;

&lt;h3&gt;
  
  
  For the Setup in This Tutorial (0.25 vCPU, 0.5 GB, 1 Task)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fargate Compute:
  vCPU:   0.25 × $0.000011244/sec × 2,592,000 sec/month = $7.29/mo
  Memory: 0.5  × $0.000001235/sec × 2,592,000 sec/month = $1.60/mo
  Subtotal: $8.89/mo

Application Load Balancer:
  Hourly:  $0.0225/hr × 720 hrs = $16.20/mo
  LCU:     ~$1–3/mo (minimal traffic)
  Subtotal: ~$17–19/mo

CloudWatch Logs: ~$0.50–1/mo

Total: ~$27–29/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compare That To...
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Same Workload (0.25 vCPU, 0.5 GB)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECS Express Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$27–29/mo&lt;/td&gt;
&lt;td&gt;Full AWS ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$16–21/mo&lt;/td&gt;
&lt;td&gt;Usage-based, no ALB cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$7–15/mo&lt;/td&gt;
&lt;td&gt;Hobby tier, limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$25–35/mo&lt;/td&gt;
&lt;td&gt;Pro plan + usage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At this scale, PaaS platforms are competitive. But as we showed in &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;last post&lt;/a&gt;, the economics flip dramatically at growth stage. Express Mode's shared ALB feature (splitting that $16/mo across 5+ services) is what makes it cost-competitive even at small scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Issues and Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ "Service is in DRAINING state"
&lt;/h3&gt;

&lt;p&gt;Your container is failing health checks. Check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Logs&lt;/strong&gt; → Look for crash errors in &lt;code&gt;/ecs/my-web-app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health check path&lt;/strong&gt; → Ensure your app responds on the configured health check endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port mapping&lt;/strong&gt; → The port in Express Mode must match what your app actually listens on&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ❌ "Unable to pull image"
&lt;/h3&gt;

&lt;p&gt;Usually an ECR permissions issue:&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;# Verify the image exists&lt;/span&gt;
aws ecr describe-images &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; my-web-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="c"&gt;# Verify the task execution role has ECR pull permissions&lt;/span&gt;
aws iam get-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; ecsTaskExecutionRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ❌ "Service stuck at 0 running tasks"
&lt;/h3&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subnet configuration&lt;/strong&gt; — Fargate tasks need subnets with internet access (public subnet with IGW, or private subnet with NAT Gateway)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security group&lt;/strong&gt; — Ensure the task security group allows outbound traffic to pull the container image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource limits&lt;/strong&gt; — You might have hit Fargate limits in your region. &lt;a href="https://docs.aws.amazon.com/general/latest/gr/ecs-service.html" rel="noopener noreferrer"&gt;Request a limit increase&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Advanced: Migrating from Express Mode to Standard ECS
&lt;/h2&gt;

&lt;p&gt;If you outgrow Express Mode's defaults, you can "eject" to full control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Note all the resources Express Mode created (cluster, service, task def, ALB)&lt;/li&gt;
&lt;li&gt;Modify them individually through the standard ECS console or CLI&lt;/li&gt;
&lt;li&gt;Express Mode doesn't prevent you from customizing — it just provides defaults&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Common reasons to eject:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need &lt;strong&gt;multi-AZ&lt;/strong&gt; load balancer configuration&lt;/li&gt;
&lt;li&gt;Need &lt;strong&gt;VPC peering&lt;/strong&gt; or &lt;strong&gt;PrivateLink&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Need &lt;strong&gt;EFS volumes&lt;/strong&gt; or &lt;strong&gt;GPU instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Need &lt;strong&gt;blue/green deployments&lt;/strong&gt; with CodeDeploy&lt;/li&gt;
&lt;li&gt;Need custom &lt;strong&gt;placement strategies&lt;/strong&gt; (e.g., spread across AZs)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;You've successfully deployed a containerized app with ECS Express Mode. Here's what to tackle next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a custom domain and HTTPS&lt;/strong&gt; — Use Route 53 + ACM to add SSL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up CI/CD&lt;/strong&gt; — We'll cover this in detail in &lt;a href="https://dev.to/parag477/cicd-github-actions-aws-ecs"&gt;How to Set Up a CI/CD Pipeline with GitHub Actions and AWS ECS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a database&lt;/strong&gt; — Connect to RDS or DynamoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure auto-scaling&lt;/strong&gt; — Fine-tune your scaling policies based on actual traffic&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simplified ECS deployment that auto-provisions ALB, security groups, scaling, and monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who it's for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developers who want App Runner simplicity with ECS power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5–10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard Fargate + ALB pricing (~$27/mo for smallest config)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key advantage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shared ALBs (save ~$16/mo per additional service)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lock-in risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero — creates standard AWS resources you fully own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Production-ready?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, with CI/CD and proper monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Even simpler?&lt;/strong&gt; If you want to skip the ECR push, CLI commands, and console clicking entirely — &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;TurboDeploy&lt;/a&gt; handles all of this in a single &lt;code&gt;git push&lt;/code&gt;. Same ECS Fargate infrastructure, same AWS pricing, zero AWS console required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;Join the waitlist →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>gettingstarted</category>
    </item>
    <item>
      <title>AWS App Runner Is Dead — Here's What You Should Use Instead (2026)</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Mon, 13 Apr 2026 12:24:24 +0000</pubDate>
      <link>https://dev.to/parag477/aws-app-runner-is-dead-heres-what-you-should-use-instead-2026-1hp7</link>
      <guid>https://dev.to/parag477/aws-app-runner-is-dead-heres-what-you-should-use-instead-2026-1hp7</guid>
      <description>&lt;p&gt;If you're running apps on AWS App Runner — or were thinking about using it — you need to read this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS has announced that App Runner will no longer accept new customers starting April 30, 2026.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't speculation. It's in the &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/what-is-apprunner.html" rel="noopener noreferrer"&gt;official AWS documentation&lt;/a&gt;. Existing customers can still use the service and create new resources, but AWS has confirmed they won't be introducing new features. The service is entering maintenance mode — which, in AWS language, is one step before eventual deprecation.&lt;/p&gt;

&lt;p&gt;If you're currently on App Runner, you need a migration plan. If you were evaluating it for a new project, you need a new direction.&lt;/p&gt;

&lt;p&gt;This post covers everything: what happened, why AWS made this call, and exactly what you should use instead — with specific migration steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was AWS App Runner?
&lt;/h2&gt;

&lt;p&gt;For those unfamiliar, App Runner was AWS's answer to Heroku/Render/Railway. Launched in 2021, it was designed to be the simplest way to deploy containerized web applications on AWS.&lt;/p&gt;

&lt;p&gt;The pitch was compelling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Point to a container image or source code repository&lt;/li&gt;
&lt;li&gt;✅ App Runner handles build, deploy, HTTPS, scaling, and load balancing&lt;/li&gt;
&lt;li&gt;✅ No need to configure VPCs, ECS clusters, task definitions, or ALBs&lt;/li&gt;
&lt;li&gt;✅ Runs on Fargate under the hood&lt;/li&gt;
&lt;li&gt;✅ Auto-scales to zero (you only pay when your app gets traffic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was genuinely good for simple use cases. If you wanted to deploy a single API or web app without learning ECS, App Runner got you there in under 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So why is AWS killing it?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why AWS Is Sunsetting App Runner
&lt;/h2&gt;

&lt;p&gt;AWS hasn't published an official "here's why" blog post, but the reasoning becomes clear when you look at the timeline:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Reason: ECS Express Mode
&lt;/h3&gt;

&lt;p&gt;In late 2025 / early 2026, AWS launched &lt;strong&gt;ECS Express Mode&lt;/strong&gt; — a new feature within Amazon ECS that provides almost everything App Runner offered, but within the ECS ecosystem.&lt;/p&gt;

&lt;p&gt;ECS Express Mode automates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cluster creation on Fargate&lt;/li&gt;
&lt;li&gt;Application Load Balancer provisioning&lt;/li&gt;
&lt;li&gt;HTTPS/TLS configuration&lt;/li&gt;
&lt;li&gt;Security groups and networking&lt;/li&gt;
&lt;li&gt;Auto-scaling policies&lt;/li&gt;
&lt;li&gt;CloudWatch monitoring and logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, ECS Express Mode makes ECS nearly as simple as App Runner — but with full access to the ECS feature set. It's App Runner's simplicity, with ECS's power.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Having two services that do essentially the same thing creates confusion and maintenance overhead for AWS.&lt;/strong&gt; App Runner was always a thin wrapper around ECS Fargate anyway. Rather than maintaining two products, AWS chose to consolidate into one — and ECS Express Mode is the survivor.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Means for the Broader Market
&lt;/h3&gt;

&lt;p&gt;This is actually a positive signal for the industry. It means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS is investing heavily in making ECS simpler&lt;/strong&gt; — which validates the core thesis that container deployment needs to be easier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "App Runner" approach of simplicity-first is the right direction&lt;/strong&gt; — AWS just decided to build it into ECS rather than maintain a separate service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS Fargate is the long-term bet&lt;/strong&gt; — if you're building on ECS Fargate today, you're on the right side of AWS's strategy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is also exactly the thesis behind &lt;a href="https://dev.to/parag477/why-we-are-building-turbodeploy"&gt;TurboDeploy&lt;/a&gt;. More on that later.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Timeline: What's Happening When
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34xdsuqbb96suqtobmnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34xdsuqbb96suqtobmnl.png" alt="AWS App Runner End of Life Timeline — May 2021 launch to TBD full deprecation" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what you need to know:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Now (April 2026)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App Runner is fully functional for existing customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;April 30, 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App Runner stops accepting &lt;strong&gt;new&lt;/strong&gt; customers. If you haven't created an App Runner service before this date, you won't be able to.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Post April 30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Existing customers can continue using App Runner, create new services, and manage workloads. AWS will maintain security and availability.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Future (date TBD)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No new features will be added. Eventually, AWS will likely announce a full sunset date.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The writing is on the wall.&lt;/strong&gt; Even if you're an existing customer, starting a migration now — while there's no time pressure — is the smart move.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Should You Use Instead?
&lt;/h2&gt;

&lt;p&gt;You have three main options, depending on your needs. Let's break down each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Amazon ECS Express Mode (AWS's Official Recommendation)
&lt;/h3&gt;

&lt;p&gt;This is the most direct replacement. AWS explicitly recommends ECS Express Mode for App Runner users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nearly identical simplicity to App Runner&lt;/li&gt;
&lt;li&gt;Provide a container image → get a running service with ALB, HTTPS, auto-scaling&lt;/li&gt;
&lt;li&gt;Full access to underlying ECS resources (you can customize later)&lt;/li&gt;
&lt;li&gt;No additional cost beyond standard Fargate + ALB pricing&lt;/li&gt;
&lt;li&gt;Shares ALBs across up to 25 services to reduce costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5llu7gkmoutbe4c2j0nf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5llu7gkmoutbe4c2j0nf.png" alt="App Runner → ECS Express Mode — Migration architecture showing what stays and what changes" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to get started with ECS Express Mode:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;Amazon ECS console&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the left nav, select &lt;strong&gt;Express mode&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your &lt;strong&gt;container image URI&lt;/strong&gt; (from ECR or any public registry)&lt;/li&gt;
&lt;li&gt;(Optional) Configure CPU, memory, environment variables&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; — done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. ECS Express Mode provisions the cluster, service, ALB, security groups, auto-scaling, and CloudWatch monitoring automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key difference from App Runner:&lt;/strong&gt; Express Mode requires a pre-built container image. App Runner could build from source code. So you'll need a CI/CD step (like GitHub Actions) to build and push your Docker image to ECR before Express Mode can deploy it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ We'll cover this end-to-end in our upcoming post: &lt;a href="https://dev.to/parag477/cicd-github-actions-aws-ecs"&gt;How to Set Up a CI/CD Pipeline with GitHub Actions and AWS ECS&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is best for:&lt;/strong&gt; Developers who are comfortable with Docker and want to stay 100% within the AWS ecosystem with minimal setup.&lt;/p&gt;




&lt;h3&gt;
  
  
  Option 2: Amazon ECS Fargate (Standard Mode)
&lt;/h3&gt;

&lt;p&gt;If you need more control than Express Mode provides — or you want to understand what's happening under the hood — standard ECS Fargate gives you full power.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete control over networking, IAM roles, task definitions, and deployment strategies&lt;/li&gt;
&lt;li&gt;Blue-green and rolling deployments&lt;/li&gt;
&lt;li&gt;Custom health check configurations&lt;/li&gt;
&lt;li&gt;Integration with any CI/CD system&lt;/li&gt;
&lt;li&gt;Multi-container task definitions (sidecars, logging agents, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The trade-off:&lt;/strong&gt; You're back to configuring VPCs, ALBs, target groups, security groups, and IAM roles manually. This is significantly more complex than Express Mode.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ We'll publish a full step-by-step guide: &lt;a href="https://dev.to/parag477/deploy-docker-container-aws-ecs-fargate"&gt;How to Deploy a Docker Container on AWS ECS Fargate&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is best for:&lt;/strong&gt; Teams with DevOps experience who need fine-grained control over their infrastructure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Option 3: A Deployment Platform That Handles the Complexity for You
&lt;/h3&gt;

&lt;p&gt;Both ECS Express Mode and standard Fargate still require you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and push Docker images yourself (or set up CI/CD)&lt;/li&gt;
&lt;li&gt;Manage your own ECR repositories&lt;/li&gt;
&lt;li&gt;Configure environment variables through AWS interfaces&lt;/li&gt;
&lt;li&gt;Deal with AWS IAM role setup&lt;/li&gt;
&lt;li&gt;Monitor deployments through the AWS console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the App Runner experience — "point to my code and deploy" — but don't want to lose the benefits of running in your own AWS account, this is where &lt;strong&gt;tools like TurboDeploy&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/parag477/why-we-are-building-turbodeploy"&gt;TurboDeploy&lt;/a&gt; sits in the gap between "PaaS simplicity" and "AWS control":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You connect your AWS account via a secure IAM role&lt;/li&gt;
&lt;li&gt;You point to your GitHub repo&lt;/li&gt;
&lt;li&gt;TurboDeploy handles the Docker build, ECR push, ECS service creation, ALB setup, and monitoring&lt;/li&gt;
&lt;li&gt;Everything runs in &lt;strong&gt;your&lt;/strong&gt; AWS account — you're not moving to someone else's cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is essentially what App Runner was supposed to be, but with the added benefit that TurboDeploy doesn't need to be an AWS service to keep working. If TurboDeploy disappears tomorrow, your ECS services keep running in your account, unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is best for:&lt;/strong&gt; Developers and startup founders who want the App Runner "just deploy it" experience without vendor dependency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ Learn more: &lt;a href="https://dev.to/parag477/why-we-are-building-turbodeploy"&gt;Why We're Building TurboDeploy — The Problem with Cloud Deployment in 2026&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison Table: Your Migration Options at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;App Runner (dying)&lt;/th&gt;
&lt;th&gt;ECS Express Mode&lt;/th&gt;
&lt;th&gt;ECS Fargate (Standard)&lt;/th&gt;
&lt;th&gt;TurboDeploy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Very Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build from source code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ Need CI/CD&lt;/td&gt;
&lt;td&gt;❌ Need CI/CD&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTPS/TLS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;⚙️ Manual (ACM + ALB)&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;⚙️ Manual config&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom domains&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ (via Route53)&lt;/td&gt;
&lt;td&gt;⚙️ Manual&lt;/td&gt;
&lt;td&gt;🔜 Roadmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full AWS resource access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Limited&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scale-to-zero&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long-term viability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Sunsetting&lt;/td&gt;
&lt;td&gt;✅ AWS investment&lt;/td&gt;
&lt;td&gt;✅ Stable&lt;/td&gt;
&lt;td&gt;✅ Active development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runs in your account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Additional cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App Runner pricing&lt;/td&gt;
&lt;td&gt;Fargate + ALB only&lt;/td&gt;
&lt;td&gt;Fargate + ALB only&lt;/td&gt;
&lt;td&gt;Fargate + ALB + TurboDeploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step-by-Step: Migrating from App Runner to ECS Express Mode
&lt;/h2&gt;

&lt;p&gt;If you have an existing App Runner service and want to migrate to ECS Express Mode, here's the recommended approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Inventory Your App Runner Services
&lt;/h3&gt;

&lt;p&gt;List all your App Runner services and document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container image source (ECR or source code)&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;CPU and memory configuration&lt;/li&gt;
&lt;li&gt;Custom domain mappings&lt;/li&gt;
&lt;li&gt;Auto-scaling configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws apprunner list-services &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Ensure Your Container Image is in ECR
&lt;/h3&gt;

&lt;p&gt;If your App Runner service builds from source code, you'll need to set up a container build pipeline first. The simplest approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;Dockerfile&lt;/code&gt; to your repository (if you don't have one)&lt;/li&gt;
&lt;li&gt;Create an ECR repository in your AWS account&lt;/li&gt;
&lt;li&gt;Set up GitHub Actions to build and push on every commit
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create an ECR repository&lt;/span&gt;
aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; my-app &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;→ Don't have a Dockerfile? Check out: &lt;a href="https://dev.to/parag477/production-ready-dockerfile-nodejs-python"&gt;How to Write a Production-Ready Dockerfile&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create an ECS Express Mode Service
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;ECS Console&lt;/strong&gt; → &lt;strong&gt;Express mode&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your ECR image URI&lt;/li&gt;
&lt;li&gt;Configure CPU, memory, and environment variables to match your App Runner settings&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your new service will be live in minutes with an AWS-managed URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Set Up DNS Migration (Blue-Green)
&lt;/h3&gt;

&lt;p&gt;AWS recommends a blue-green approach for zero-downtime migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Route 53 &lt;strong&gt;weighted routing&lt;/strong&gt; record set&lt;/li&gt;
&lt;li&gt;Point 10% of traffic to the new ECS Express service&lt;/li&gt;
&lt;li&gt;Monitor for errors, latency, and functionality&lt;/li&gt;
&lt;li&gt;Gradually increase to 25% → 50% → 100%&lt;/li&gt;
&lt;li&gt;Decommission the App Runner service once all traffic is migrated
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Example Route 53 weighted record
my-app.example.com → App Runner URL (weight: 90)
my-app.example.com → ECS ALB URL    (weight: 10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Tear Down App Runner
&lt;/h3&gt;

&lt;p&gt;Once all traffic is on ECS and you're confident everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws apprunner delete-service &lt;span class="nt"&gt;--service-arn&lt;/span&gt; arn:aws:apprunner:us-east-1:123456789:service/my-app/xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What About Non-AWS Alternatives?
&lt;/h2&gt;

&lt;p&gt;If the App Runner sunset makes you question your AWS commitment entirely, here are your options outside the AWS ecosystem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick deployments, modern DX&lt;/td&gt;
&lt;td&gt;Usage-based pricing, you don't own infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predictable pricing, easy setup&lt;/td&gt;
&lt;td&gt;Less flexibility than AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fly.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global edge deployment&lt;/td&gt;
&lt;td&gt;More complex networking model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Coolify&lt;/strong&gt; (self-hosted)&lt;/td&gt;
&lt;td&gt;Full control on your own VPS&lt;/td&gt;
&lt;td&gt;You manage the PaaS layer + the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are all solid options, but they come with the &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;platform tax&lt;/a&gt; trade-off: you're paying for convenience with someone else's infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture: What This Tells Us About Cloud Computing in 2026
&lt;/h2&gt;

&lt;p&gt;The App Runner sunset isn't just a product lifecycle event. It's a signal about where cloud deployment is heading:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Simplicity is becoming a feature of existing services, not separate products
&lt;/h3&gt;

&lt;p&gt;Instead of creating new "simple" services, cloud providers are making their core services simpler. ECS Express Mode is ECS-made-easy, not a separate product. Expect the same pattern from other AWS services.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Containers on Fargate is the "default" deployment model
&lt;/h3&gt;

&lt;p&gt;AWS is clearly betting on ECS Fargate as the standard way to deploy containerized applications. Not Lambda (too limited for many workloads), not EKS (too complex for most teams), but ECS on Fargate.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ We explore this further in: &lt;a href="https://dev.to/parag477/ecs-vs-eks-vs-lambda-choose-right-aws-compute"&gt;ECS vs EKS vs Lambda — How to Choose the Right AWS Compute Service&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The developer experience gap is still the real problem
&lt;/h3&gt;

&lt;p&gt;Even with ECS Express Mode, deploying to AWS is still harder than Vercel or Railway. The gap is smaller than it was in 2021, but it's still there. And that gap is exactly the space where tools like TurboDeploy add value.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR — What To Do Right Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're currently on App Runner:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Don't panic — your services keep running&lt;/li&gt;
&lt;li&gt;✅ Start planning migration to ECS Express Mode&lt;/li&gt;
&lt;li&gt;✅ Set up a Docker build pipeline if you don't have one&lt;/li&gt;
&lt;li&gt;✅ Test ECS Express Mode with a non-critical service first&lt;/li&gt;
&lt;li&gt;✅ Migrate production services using blue-green (Route 53 weighted routing)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If you were considering App Runner for a new project:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Use &lt;strong&gt;ECS Express Mode&lt;/strong&gt; for the closest experience&lt;/li&gt;
&lt;li&gt;✅ Or try &lt;strong&gt;&lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;TurboDeploy&lt;/a&gt;&lt;/strong&gt; for an even simpler experience that deploys to your AWS account&lt;/li&gt;
&lt;li&gt;❌ Don't start with App Runner — the service is entering end-of-life&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If you're unsure which option is right:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Read our decision guide → &lt;a href="https://dev.to/parag477/paas-vs-aws-for-your-startup"&gt;How to Choose Between a PaaS and AWS for Your Startup&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;Join the TurboDeploy waitlist →&lt;/a&gt;&lt;/strong&gt; — Deploy to AWS without the AWS headache.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Why We're Building TurboDeploy - The Problem with Cloud Deployment in 2026</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Thu, 09 Apr 2026 15:42:02 +0000</pubDate>
      <link>https://dev.to/parag477/why-were-building-turbodeploy-the-problem-with-cloud-deployment-in-2026-5977</link>
      <guid>https://dev.to/parag477/why-were-building-turbodeploy-the-problem-with-cloud-deployment-in-2026-5977</guid>
      <description>&lt;p&gt;Every developer has been there.&lt;/p&gt;

&lt;p&gt;You've built something — an app, a side project, maybe the MVP of your startup. It works on your machine. It runs beautifully in development. And now you need to put it on the internet.&lt;/p&gt;

&lt;p&gt;That's when the fun stops.&lt;/p&gt;

&lt;p&gt;If you go with AWS, you're suddenly drowning in a sea of IAM roles, VPCs, security groups, task definitions, load balancers, target groups and CloudFormation templates. You didn't sign up for a PhD in infrastructure, but here you are, three hours in, Googling "what is an ECS cluster" at 2 AM.&lt;/p&gt;

&lt;p&gt;If you go with a managed platform like Vercel, Railway or Render, the experience is beautiful. Connect your repo, click deploy and you're live. But this simplicity has a price. Literally.&lt;/p&gt;

&lt;p&gt;A few months later, your startup starts growing. And that $0/month hobby plan quietly becomes a $200/month, then a $500/month, then a "wait, why is my cloud bill higher than my rent?" situation.&lt;/p&gt;

&lt;p&gt;We've lived through both of these nightmares. And that's exactly why we're building &lt;strong&gt;TurboDeploy&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Extremes of Cloud Deployment
&lt;/h2&gt;

&lt;p&gt;In 2026, deploying a web application still forces you into one of two painful choices:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: The AWS Way (Powerful but painful)
&lt;/h3&gt;

&lt;p&gt;AWS is the gold standard of cloud infrastructure. It's what Netflix, Airbnb and thousands of serious companies run on. It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full control over every aspect of your infrastructure&lt;/li&gt;
&lt;li&gt;Transparent, pay-for-what-you-use pricing&lt;/li&gt;
&lt;li&gt;No vendor lock-in (it's your account, your resources)&lt;/li&gt;
&lt;li&gt;Enterprise-grade security, compliance, and scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the learning curve is &lt;em&gt;brutal&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Just to deploy a simple Node.js app, you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a VPC with subnets&lt;/li&gt;
&lt;li&gt;Set up security groups&lt;/li&gt;
&lt;li&gt;Create an ECR repository&lt;/li&gt;
&lt;li&gt;Build and push a Docker image&lt;/li&gt;
&lt;li&gt;Write an ECS task definition&lt;/li&gt;
&lt;li&gt;Create an ECS service&lt;/li&gt;
&lt;li&gt;Configure an Application Load Balancer&lt;/li&gt;
&lt;li&gt;Set up target groups and listeners&lt;/li&gt;
&lt;li&gt;Configure IAM roles and policies&lt;/li&gt;
&lt;li&gt;Set up CloudWatch logging&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's &lt;strong&gt;10 steps&lt;/strong&gt; before your "Hello World" is live. And if you get one IAM permission wrong, you get a cryptic error message that sends you down a 45-minute Stack Overflow rabbit hole.&lt;/p&gt;

&lt;p&gt;Most developers give up somewhere around step 4.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: The PaaS Way (Easy but expensive)
&lt;/h3&gt;

&lt;p&gt;Platforms like Vercel, Railway, Render, Heroku, and Fly.io exist because Option A is so painful. And they're genuinely excellent products. The developer experience is world-class.&lt;/p&gt;

&lt;p&gt;But here's what they don't tell you upfront:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They host your application on &lt;em&gt;their&lt;/em&gt; infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're paying a "platform tax"&lt;/strong&gt;: They buy AWS/GCP resources at bulk prices, mark them up 2–5x and resell them to you as "simplified deployment."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your costs scale with &lt;em&gt;their&lt;/em&gt; pricing, not AWS pricing&lt;/strong&gt;: When your app grows, you don't benefit from AWS's economies of scale — you benefit from &lt;em&gt;their&lt;/em&gt; price tiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're locked in&lt;/strong&gt;: Your infrastructure definitions, environment configurations and deployment pipelines are all proprietary. Moving to AWS later means starting from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You don't own your infrastructure&lt;/strong&gt;: Your containers run on their accounts. Your logs are in their dashboard. Your data flows through their network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzpykzqyvp4bdoktq38n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzpykzqyvp4bdoktq38n.png" alt="PaaS costs vs. direct AWS deployment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me put real numbers on this. Here's what a simple web app (2 vCPU, 4 GB RAM, running 24/7) costs per month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Monthly Cost (approx.)&lt;/th&gt;
&lt;th&gt;What You Get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; (Pro)&lt;/td&gt;
&lt;td&gt;$20/seat + usage&lt;/td&gt;
&lt;td&gt;Great for frontend, but serverless function costs and bandwidth add up fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$50–150+&lt;/td&gt;
&lt;td&gt;Usage-based, but grows linearly with traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20–85 per service&lt;/td&gt;
&lt;td&gt;Predictable, but no flexibility in underlying infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AWS ECS Fargate&lt;/strong&gt; (direct)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$30–50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same compute, full control, your account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At small scale, the difference is $20–50/month. Not a big deal.&lt;/p&gt;

&lt;p&gt;At growth scale (let's say 5 services, production + staging environments, a database and a background worker), the difference becomes &lt;strong&gt;$500–1500/month vs. $150–300/month on AWS direct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's $3,600–$14,400/year in "platform tax." For an early-stage startup watching every dollar, that's the difference between hiring a part-time contractor and not.&lt;/p&gt;




&lt;h2&gt;
  
  
  There Should Be a Third Option
&lt;/h2&gt;

&lt;p&gt;We kept asking ourselves the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What if you could have the simplicity of Vercel — but everything runs in your own AWS account?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No markup. No lock-in. No "we host your app on our cloud and charge you 3x."&lt;/p&gt;

&lt;p&gt;Just connect your AWS account, point to your repo, click deploy — and everything gets provisioned in &lt;em&gt;your&lt;/em&gt; cloud, with &lt;em&gt;your&lt;/em&gt; billing, under &lt;em&gt;your&lt;/em&gt; control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's TurboDeploy.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What TurboDeploy Actually Does
&lt;/h2&gt;

&lt;p&gt;TurboDeploy is a deployment platform for AWS. But unlike traditional PaaS platforms, we never become your hosting provider. &lt;strong&gt;AWS is your hosting provider. We're just the orchestration layer that removes the pain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You connect your AWS account&lt;/strong&gt; — using a secure, read-scoped IAM role (via AWS AssumeRole). No long-term access keys, ever. We create a narrowly scoped role that can only do what's needed for deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You point to your GitHub repo&lt;/strong&gt; — or upload your project. Tell us the build command, start command, and port.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You click deploy&lt;/strong&gt; — and everything happens automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We pull your code&lt;/li&gt;
&lt;li&gt;Build a Docker image&lt;/li&gt;
&lt;li&gt;Push it to Amazon ECR &lt;em&gt;in your account&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Create an ECS task definition&lt;/li&gt;
&lt;li&gt;Provision an ECS Fargate service behind an Application Load Balancer&lt;/li&gt;
&lt;li&gt;Configure logging via CloudWatch&lt;/li&gt;
&lt;li&gt;Return a public URL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You see everything in your dashboard&lt;/strong&gt; — deployment status, logs, errors, and your live URL.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The key difference&lt;/strong&gt;: After deployment, if you go to &lt;em&gt;your&lt;/em&gt; AWS console, you'll see the ECS service, the load balancer, the CloudWatch logs — all in your account. If you ever stop using TurboDeploy, your app keeps running. Nothing changes. Because it was never on our infrastructure to begin with.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture (Simplified)
&lt;/h2&gt;

&lt;p&gt;For the technical readers, here's what's happening under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐     ┌──────────────────┐     ┌─────────────────────────────────┐
│  Your Code  │────▶│   TurboDeploy    │────▶│    YOUR AWS Account             │
│  (GitHub)   │     │  (Orchestration)  │     │                                 │
└─────────────┘     └──────────────────┘     │  ┌─────┐  ┌──────┐  ┌──────┐  │
                           │                  │  │ ECR │─▶│ ECS  │─▶│ ALB  │──│──▶ Users
                     AssumeRole               │  └─────┘  │Fargate│  └──────┘  │
                    (temp creds)              │           └──────┘             │
                                              │  ┌──────────────┐             │
                                              │  │  CloudWatch   │             │
                                              │  │  (Logs)       │             │
                                              │  └──────────────┘             │
                                              └─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security model&lt;/strong&gt;: TurboDeploy uses AWS AssumeRole to get temporary, scoped credentials. We never store permanent AWS access keys. The IAM role we create has the minimum permissions needed — it can push images, manage ECS services, and configure load balancers. It can't access your S3 buckets, databases, or anything else.&lt;/p&gt;

&lt;p&gt;We also never see or touch your application data. We only orchestrate infrastructure creation. Your code runs entirely in your account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We're Building This Now
&lt;/h2&gt;

&lt;p&gt;Three things converged in 2026 that make this the right moment:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AWS just made ECS dramatically simpler
&lt;/h3&gt;

&lt;p&gt;AWS launched &lt;strong&gt;ECS Express Mode&lt;/strong&gt; — a streamlined way to deploy containers on Fargate with automated ALB, auto-scaling, and security group provisioning. This is a signal that even AWS recognizes the deployment experience needs to be simpler. TurboDeploy builds on top of this foundation to make it even easier.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ Read our deep dive: &lt;a href="https://dev.to/parag477/aws-ecs-express-mode-getting-started-guide"&gt;AWS ECS Express Mode: The Complete Getting Started Guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The "platform tax" backlash is real
&lt;/h3&gt;

&lt;p&gt;Developers are increasingly vocal about PaaS pricing. Reddit threads in r/devops and r/webdev are full of developers sharing "bill shock" stories — especially from Vercel. The shift back to owning infrastructure is happening, but the tooling gap makes it hard for most teams to make the switch.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;→ We break down the numbers: &lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;The Hidden Costs of Vercel, Railway, and Render — A Real-World Pricing Breakdown&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. "Bring your own cloud" is becoming a real pattern
&lt;/h3&gt;

&lt;p&gt;We're not the only ones who see this trend. But most tools in this space are either too complex (Terraform, Pulumi — great but require IaC expertise) or too opinionated (Coolify, Dokploy — deploy to VPS, not to your cloud account). TurboDeploy sits in the sweet spot: opinionated enough to be simple, flexible enough to use real AWS infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What TurboDeploy Is NOT
&lt;/h2&gt;

&lt;p&gt;Let's be honest about our scope. This matters more than marketing promises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Not a full AWS management console&lt;/strong&gt; — We do one thing: deploy containerized web apps to ECS Fargate.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Not a database provisioning tool&lt;/strong&gt; — (Yet. It's on the roadmap, but not in v1.)&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Not for multi-service microservice architectures&lt;/strong&gt; — (Also roadmap. v1 is single-container apps.)&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Not Kubernetes&lt;/strong&gt; — We deliberately chose ECS Fargate over EKS because most startups don't need Kubernetes.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Not a hosting provider&lt;/strong&gt; — We never host your app. Period.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're intentionally narrow. We'd rather do one thing perfectly than ten things poorly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;TurboDeploy is for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo developers&lt;/strong&gt; who want AWS-grade infrastructure without the AWS learning curve&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early-stage startup founders&lt;/strong&gt; who want to own their infrastructure from day one without hiring a DevOps engineer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small teams&lt;/strong&gt; who are currently on Vercel/Railway/Render and starting to feel the cost pressure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie hackers&lt;/strong&gt; who want cloud costs measured in dollars, not hundreds of dollars&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if you're a 200-person engineering org with a dedicated platform team, then also we can probably help you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where We Are Today
&lt;/h2&gt;

&lt;p&gt;We're being transparent about this: &lt;strong&gt;TurboDeploy is in active development.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our MVP can deploy a single containerized app (Node.js, Python, etc.) to AWS ECS Fargate in your account, with a load balancer and a public URL. It works. We've tested it.&lt;/p&gt;

&lt;p&gt;But we're not pretending it's production-ready for everyone. Features like custom domains, auto-rollback, preview environments, and multi-region support are on the roadmap, not in the product today.&lt;/p&gt;

&lt;p&gt;We've launched a waitlist at &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;turbodeploy.dev&lt;/a&gt; and we're looking for early adopters who want to try it, break it and help us figure out what to build next.&lt;/p&gt;

&lt;p&gt;If this sounds like you, sign up. We'll personally onboard you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming Next
&lt;/h2&gt;

&lt;p&gt;Over the next few weeks, we'll be publishing deep dives on the topics that matter to developers building on AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/parag477/hidden-costs-vercel-railway-render-pricing-breakdown"&gt;The Hidden Costs of Vercel, Railway, and Render — A Real-World Pricing Breakdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/parag477/aws-ecs-express-mode-getting-started-guide"&gt;AWS ECS Express Mode: The Complete Getting Started Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/parag477/deploy-docker-container-aws-ecs-fargate"&gt;How to Deploy a Docker Container on AWS ECS Fargate — Step by Step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/parag477/aws-iam-roles-explained-beginners-guide"&gt;AWS IAM Roles Explained — A Beginner's Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow along. We're building in public, and we're not pretending we have all the answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Cloud deployment in 2026 is stuck between two bad options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS gives you power but drowns you in complexity.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;PaaS gives you simplicity but charges you a premium for it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TurboDeploy is the third option: &lt;strong&gt;the simplicity of a PaaS, running entirely in your own AWS account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your code. Your AWS. No lock-in.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://turbodeploy.dev" rel="noopener noreferrer"&gt;Join the waitlist at turbodeploy.dev →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>startup</category>
      <category>clouddeployment</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Unlocking the Potential: Exploring the Future of Web Development with React</title>
      <dc:creator>Parag Agrawal</dc:creator>
      <pubDate>Tue, 30 May 2023 14:41:01 +0000</pubDate>
      <link>https://dev.to/parag477/unlocking-the-potential-exploring-the-future-of-web-development-with-react-2e0n</link>
      <guid>https://dev.to/parag477/unlocking-the-potential-exploring-the-future-of-web-development-with-react-2e0n</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Web development has come a long way, and the future looks incredibly promising. One technology that has gained significant popularity and transformed the web development landscape is React. In this article, we will delve into the potential that React brings to the table, unlocking new possibilities and empowering developers to create exceptional web experiences. Whether you're a seasoned developer or just starting out, understanding the power of React is crucial to stay ahead in the ever-evolving world of web development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is React?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4abk4xs9u04cy4tv8gv2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4abk4xs9u04cy4tv8gv2.jpg" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At its core, React is a JavaScript library for building user interfaces. Developed by Facebook, React provides a declarative and component-based approach, making it easier to build complex UIs by breaking them down into reusable components. With its virtual DOM and efficient rendering techniques, React has gained popularity for its ability to create highly interactive and performant applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unlocking the Potential: Exploring the Key Features of React
&lt;/h2&gt;

&lt;p&gt;React is packed with powerful features that enhance the development process and enable developers to create dynamic web applications. Let's take a closer look at some of these features:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Component-Based Architecture
&lt;/h3&gt;

&lt;p&gt;One of the fundamental concepts of React is its component-based architecture. By breaking down the user interface into reusable components, developers can build complex applications more efficiently. Components are self-contained and can be composed together, offering a modular and scalable approach to web development.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Virtual DOM
&lt;/h3&gt;

&lt;p&gt;React utilizes a virtual DOM, which is a lightweight representation of the actual DOM. This allows React to efficiently update and render only the necessary components when changes occur, minimizing the impact on performance. The virtual DOM acts as a middle layer between the actual DOM and the application's state, enabling faster rendering and improved user experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. JSX: The Power of JavaScript and HTML Fusion
&lt;/h3&gt;

&lt;p&gt;React introduces JSX, a syntax extension that combines JavaScript and HTML. This powerful fusion allows developers to write HTML-like code within JavaScript, making it easier to create dynamic and interactive user interfaces. JSX enhances code readability and simplifies the process of building complex UI structures.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unidirectional Data Flow
&lt;/h3&gt;

&lt;p&gt;React follows a unidirectional data flow, which means data flows in a single direction within the application. This clear data flow pattern makes it easier to track and manage data changes, resulting in more predictable and maintainable code. Unidirectional data flow enhances code stability and makes debugging and testing more efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. React Native: Cross-Platform Development
&lt;/h3&gt;

&lt;p&gt;React extends its potential beyond the web with React Native, a framework that enables developers to build native mobile applications using React. By leveraging React's component-based approach, developers can create mobile apps that are indistinguishable from those built using native languages like Java or Swift. React Native allows for code reusability, reducing development time and effort across multiple platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Performance Optimization
&lt;/h3&gt;

&lt;p&gt;React's virtual DOM and efficient diffing algorithm contribute to superior performance. By minimizing unnecessary updates to the actual DOM, React ensures optimal rendering speed. Additionally, React's ability to handle large datasets and its support for server-side rendering (SSR) enhance the overall performance of web applications.&lt;/p&gt;

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

&lt;p&gt;React has undoubtedly unlocked a world of possibilities in web development. Its component-based architecture, virtual DOM, JSX syntax, and other powerful features empower developers to create dynamic, performant, and scalable applications. Whether you're building a simple website, a complex web application, or even a mobile app using React Native, you can harness the potential of React to deliver exceptional user experiences.&lt;/p&gt;

&lt;p&gt;As the web continues to evolve, React remains at the forefront of web development technologies, constantly evolving and improving. By investing time in learning React and keeping up with its advancements, developers can stay ahead of the curve and unlock the full potential of web development.&lt;/p&gt;

&lt;p&gt;In conclusion, Unlocking the Potential: Exploring the Future of Web Development with React is not just a catchy title; it represents the exciting journey that developers can embark upon with React. So, dive in, explore the vast ecosystem of React, and embrace the future of web development.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>react</category>
    </item>
  </channel>
</rss>
