<?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: Sanghun Yun</title>
    <description>The latest articles on DEV Community by Sanghun Yun (@gearflowlab).</description>
    <link>https://dev.to/gearflowlab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4174505%2F3beaa14d-6171-4b38-b6ff-d51f55322d0d.png</url>
      <title>DEV Community: Sanghun Yun</title>
      <link>https://dev.to/gearflowlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gearflowlab"/>
    <language>en</language>
    <item>
      <title>Best Cloud VPS for Self-Hosting Docker &amp; Automation in 2026: DigitalOcean vs Hetzner vs AWS Lightsail</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:47:52 +0000</pubDate>
      <link>https://dev.to/gearflowlab/best-cloud-vps-for-self-hosting-docker-automation-in-2026-digitalocean-vs-hetzner-vs-aws-49d5</link>
      <guid>https://dev.to/gearflowlab/best-cloud-vps-for-self-hosting-docker-automation-in-2026-digitalocean-vs-hetzner-vs-aws-49d5</guid>
      <description>&lt;p&gt;In 2026, software engineers, devops teams, and bootstrapped technical founders are actively reclaiming ownership of their software stack. By self-hosting open-source platforms like n8n, Supabase, PostgreSQL, and Plausible Analytics, teams save thousands of dollars each month compared to traditional SaaS tools like Zapier and Airtable.&lt;/p&gt;

&lt;p&gt;However, self-hosting is only as reliable as your cloud infrastructure. Choosing the wrong Virtual Private Server (VPS) results in throttled burst CPUs, unexpected data transfer surcharges, and sudden account suspensions during automated fraud checks.&lt;/p&gt;

&lt;p&gt;In this benchmark guide, we compare the top three cloud VPS providers: &lt;strong&gt;DigitalOcean Droplets&lt;/strong&gt;, &lt;strong&gt;Hetzner Cloud&lt;/strong&gt;, and &lt;strong&gt;AWS Lightsail&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision Matrix
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Start["Self-Hosting Docker &amp;amp; Automation Workloads (2026)"] --&amp;gt; NeedCheck{"What is your primary decision criteria?"}

    NeedCheck --&amp;gt;|Simplicity, Reliable Support &amp;amp; Global Fast Edge| DOPriority["Need 1-Click Docker Marketplace,&amp;lt;br/&amp;gt;Zero Setup Friction &amp;amp; Global Datacenters?"]
    NeedCheck --&amp;gt;|Lowest Raw Hardware Cost in Europe| HetznerPriority["Experienced Linux Sysadmin &amp;amp;&amp;lt;br/&amp;gt;Strictly European Low Latency?"]
    NeedCheck --&amp;gt;|Existing AWS Cloud Footprint| AWSPriority["Existing Enterprise AWS Architecture&amp;lt;br/&amp;gt;(VPC Peering, IAM, RDS)?"]

    DOPriority --&amp;gt; DO["DigitalOcean Droplets (Top Recommendation)&amp;lt;br/&amp;gt;- $4 to $6/mo Entry Points&amp;lt;br/&amp;gt;- 1-Click Docker App Marketplace&amp;lt;br/&amp;gt;- $200 Free Developer Credit&amp;lt;br/&amp;gt;- Predictable Bandwidth &amp;amp; Global SLA"]

    HetznerPriority --&amp;gt; Hetzner["Hetzner Cloud&amp;lt;br/&amp;gt;- High raw CPU per dollar&amp;lt;br/&amp;gt;- Primarily European regions&amp;lt;br/&amp;gt;- Strict manual KYC approval&amp;lt;br/&amp;gt;- DIY networking &amp;amp; firewall management"]

    AWSPriority --&amp;gt; AWS["AWS Lightsail&amp;lt;br/&amp;gt;- Native integration with AWS APIs&amp;lt;br/&amp;gt;- Steeper pricing over time&amp;lt;br/&amp;gt;- Slower web management dashboard&amp;lt;br/&amp;gt;- Unexpected data egress surprises"]&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  2026 Cloud VPS Comparison Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature / Metric&lt;/th&gt;
&lt;th&gt;DigitalOcean Droplets&lt;/th&gt;
&lt;th&gt;Hetzner Cloud&lt;/th&gt;
&lt;th&gt;AWS Lightsail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entry Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.00 – $6.00 / mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~€4.50 / mo (~$4.90)&lt;/td&gt;
&lt;td&gt;$3.50 – $5.00 / mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM &amp;amp; vCPU (Entry)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 GB RAM / 1 vCPU&lt;/td&gt;
&lt;td&gt;2 GB RAM / 1 vCPU&lt;/td&gt;
&lt;td&gt;1 GB RAM / 1 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;High-Speed NVMe SSD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVMe SSD&lt;/td&gt;
&lt;td&gt;Standard SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Included Bandwidth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,000 GB (1 TB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 TB (Europe only)&lt;/td&gt;
&lt;td&gt;1,000 GB (1 TB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Global Datacenters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15+ (US, EU, Asia, India, Aus)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 (Primarily Germany/Finland)&lt;/td&gt;
&lt;td&gt;13 (Global AWS AZs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1-Click Docker Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (Official Marketplace)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (Manual setup)&lt;/td&gt;
&lt;td&gt;No (Basic OS only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Account Activation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Instant (Zero KYC friction)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strict (Manual ID/Passport review)&lt;/td&gt;
&lt;td&gt;Complex (AWS IAM setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New Account Bonus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$200 for 60 Days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;3 Months Free (Select tiers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. DigitalOcean Droplets (Score: 9.6/10) - Overall Winner
&lt;/h2&gt;

&lt;p&gt;DigitalOcean remains the developer community's top choice because it eliminates operational friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1-Click Docker Marketplace Image:&lt;/strong&gt; You can spin up a fully configured, security-hardened Docker and Docker Compose environment on Ubuntu in 50 seconds without manual package installation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15+ Global Datacenter Regions:&lt;/strong&gt; From New York and San Francisco to Frankfurt, Singapore, Bangalore, and Sydney, your webhooks and APIs enjoy ultra-low latency worldwide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent Pooled Bandwidth:&lt;/strong&gt; Every Droplet includes 1,000 GB of outbound transfer, which pools across your entire account, preventing unexpected egress billing spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frictionless Onboarding:&lt;/strong&gt; Instant account approval with credit card or PayPal, with zero invasive passport verification hurdles.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Hetzner Cloud (Score: 8.8/10) - The European Heavyweight
&lt;/h2&gt;

&lt;p&gt;Hetzner is famous on Reddit (&lt;code&gt;r/selfhosted&lt;/code&gt;) for offering incredible raw CPU performance per euro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pros:&lt;/strong&gt; 2 GB of RAM and 20 TB of bandwidth for ~€4.50/month in German and Finnish datacenters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cons:&lt;/strong&gt; An aggressive fraud detection algorithm frequently terminates accounts of non-EU developers or demands unredacted passport uploads. Furthermore, with virtually no presence in Asia or South America, international webhook latency can exceed 250ms.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. AWS Lightsail (Score: 8.2/10) - The Enterprise Bridge
&lt;/h2&gt;

&lt;p&gt;AWS Lightsail is Amazon's simplified VPS tier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pros:&lt;/strong&gt; Native, private VPC peering directly into AWS managed databases (RDS Aurora) and Amazon S3 buckets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cons:&lt;/strong&gt; A sluggish web console, complex snapshot rotation, burstable CPU credit throttles during continuous batch processing, and steep $0.09/GB egress fees if you exceed bundle limits.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Verdict
&lt;/h2&gt;

&lt;p&gt;If your time is valuable and you want high-performance NVMe storage, 1-Click Docker convenience, and datacenters close to your global users, &lt;strong&gt;DigitalOcean Droplets are the clear winner for 2026&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gearflowlab.com/best-cloud-vps-self-hosting-docker-digitalocean-hetzner-aws/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to Migrate Local Docker &amp; n8n from WSL2 to a $6 DigitalOcean Droplet</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:46:45 +0000</pubDate>
      <link>https://dev.to/gearflowlab/how-to-migrate-local-docker-n8n-from-wsl2-to-a-6-digitalocean-droplet-55ak</link>
      <guid>https://dev.to/gearflowlab/how-to-migrate-local-docker-n8n-from-wsl2-to-a-6-digitalocean-droplet-55ak</guid>
      <description>&lt;p&gt;Running mission-critical automation workflows (n8n, PostgreSQL, Redis, web scrapers) on Windows 11 via WSL2 inevitably hits two fatal operational bottlenecks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vmmem&lt;/code&gt; RAM Ballooning:&lt;/strong&gt; The Linux page cache greedily eats 8 to 16 GB of host RAM, starving your IDE and browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline Webhooks:&lt;/strong&gt; Whenever your laptop sleeps, reboots for Windows Updates, or drops Wi-Fi, incoming Stripe, GitHub, or CRM webhooks fail with silent timeouts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this guide, we'll execute an atomic volume snapshot in WSL2, securely transfer it to a &lt;strong&gt;$6/month DigitalOcean Basic Droplet&lt;/strong&gt;, and configure production Nginx with Let's Encrypt SSL for 24/7 always-on reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Architecture
&lt;/h2&gt;

&lt;p&gt;Here is the migration pipeline from local WSL2 to a hardened cloud droplet:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph LocalWSL["1. Local Development Environment (WSL2)"]
        WSL["Local Windows 11 PC&amp;lt;br/&amp;gt;- vmmem RAM exhaustion&amp;lt;br/&amp;gt;- Shuts down when PC sleeps"]
        DockerLocal["Local Docker Containers&amp;lt;br/&amp;gt;(n8n, Postgres, Redis)"]
        Dump["Volume Snapshot Export&amp;lt;br/&amp;gt;docker run --rm -v ... tar -czf"]
    end

    subgraph Transit["2. Secure Network Migration"]
        RSync["Encrypted SSH Sync&amp;lt;br/&amp;gt;rsync -avz -e ssh backup.tar.gz root@droplet:/srv/"]
    end

    subgraph DigitalOcean["3. DigitalOcean Cloud Droplet ($6/mo Standard)"]
        UFW["UFW Firewall Protection&amp;lt;br/&amp;gt;(Open 22, 80, 443 | Block raw app ports)"]
        DockerHost["Docker Engine + Compose&amp;lt;br/&amp;gt;(24/7 Dedicated Cloud Compute)"]

        subgraph Stack["Production Microservices Stack"]
            Nginx["Nginx Reverse Proxy&amp;lt;br/&amp;gt;(Let's Encrypt SSL Auto-Renewal)"]
            N8nProd["n8n Automation Engine&amp;lt;br/&amp;gt;(Webhooks Active 24/7)"]
            DBProd[("Restored Persistent Volumes&amp;lt;br/&amp;gt;(Fast NVMe SSD Storage)")]
        end
    end

    DockerLocal --&amp;gt; Dump
    Dump --&amp;gt; RSync
    RSync --&amp;gt; UFW
    UFW --&amp;gt; DockerHost
    DockerHost --&amp;gt; Nginx
    Nginx --&amp;gt;|HTTPS Reverse Proxy| N8nProd
    N8nProd --&amp;gt; DBProd&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  Step 1: Provision and Secure the Cloud Droplet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Spin up an &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; Droplet on DigitalOcean (Basic $6/mo tier: 1 GB RAM / 1 vCPU / 25 GB NVMe SSD).&lt;/li&gt;
&lt;li&gt;SSH into your droplet and configure the host firewall with UFW:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@&amp;lt;droplet_ip&amp;gt;

&lt;span class="c"&gt;# Restrict inbound traffic strictly to SSH, HTTP, and HTTPS&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 22/tcp comment &lt;span class="s1"&gt;'SSH'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp comment &lt;span class="s1"&gt;'HTTP'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp comment &lt;span class="s1"&gt;'HTTPS'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Never expose port 5678 (n8n) or 5432 (Postgres) directly to the public internet.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Atomic Volume Snapshot in WSL2
&lt;/h2&gt;

&lt;p&gt;Stop your local stack cleanly to guarantee zero open file-lock corruption:&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;# Inside local WSL2 terminal&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/my-n8n-project
docker compose stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a point-in-time tarball snapshot of your persistent named volume using a disposable Alpine container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/data:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &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;:/backup &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; /backup/n8n_data_export.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Transfer Volume &amp;amp; Configuration Over SSH
&lt;/h2&gt;

&lt;p&gt;Push the archive, your &lt;code&gt;docker-compose.yml&lt;/code&gt;, and your &lt;code&gt;.env&lt;/code&gt; file directly to the Droplet:&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;# Run from local WSL2&lt;/span&gt;
scp &lt;span class="nt"&gt;-P&lt;/span&gt; 22 n8n_data_export.tar.gz root@&amp;lt;droplet_ip&amp;gt;:/srv/
scp &lt;span class="nt"&gt;-P&lt;/span&gt; 22 docker-compose.yml root@&amp;lt;droplet_ip&amp;gt;:/srv/
scp &lt;span class="nt"&gt;-P&lt;/span&gt; 22 .env root@&amp;lt;droplet_ip&amp;gt;:/srv/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Restore Volume and Boot Production Stack
&lt;/h2&gt;

&lt;p&gt;SSH into the cloud server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@&amp;lt;droplet_ip&amp;gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /srv

&lt;span class="c"&gt;# 1. Create target named volume&lt;/span&gt;
docker volume create n8n_data

&lt;span class="c"&gt;# 2. Extract backup into cloud volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /srv:/backup &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /backup/n8n_data_export.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Production &lt;code&gt;docker-compose.yml&lt;/code&gt;
&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&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;docker.n8n.io/n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-automation&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&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;N8N_HOST=n8n.yourdomain.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=5678&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=https://n8n.yourdomain.com/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=UTC&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PAYLOAD_SIZE_MAX=64&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;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-net&lt;/span&gt;

  &lt;span class="na"&gt;nginx&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;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-proxy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&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;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&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;./nginx.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certbot/conf:/etc/letsencrypt:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certbot/www:/var/www/certbot:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-net&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&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;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Issue SSL and Launch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Point your domain &lt;code&gt;A&lt;/code&gt; record (&lt;code&gt;n8n.yourdomain.com&lt;/code&gt;) to your Droplet IP.&lt;/li&gt;
&lt;li&gt;Issue Let's Encrypt certificates:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-v&lt;/span&gt; /srv/certbot/conf:/etc/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-v&lt;/span&gt; /srv/certbot/www:/var/www/certbot &lt;span class="se"&gt;\&lt;/span&gt;
     certbot/certbot certonly &lt;span class="nt"&gt;--webroot&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; /var/www/certbot &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; n8n.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the stack:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your workflows, API keys, database connections, and executions are now running 24/7 in the cloud without consuming a single megabyte of memory on your local machine.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gearflowlab.com/migrate-local-docker-n8n-wsl2-digitalocean-droplet/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>n8n</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How to Secure the Docker Daemon Socket: Preventing Root Escalation &amp; Container Escapes</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:30:13 +0000</pubDate>
      <link>https://dev.to/gearflowlab/how-to-secure-the-docker-daemon-socket-preventing-root-escalation-container-escapes-4hde</link>
      <guid>https://dev.to/gearflowlab/how-to-secure-the-docker-daemon-socket-preventing-root-escalation-container-escapes-4hde</guid>
      <description>&lt;p&gt;Mounting the Docker Unix socket (&lt;code&gt;/var/run/docker.sock&lt;/code&gt;) into an application container is one of the most dangerous anti-patterns in modern DevOps. &lt;/p&gt;

&lt;p&gt;While tools like Portainer, Traefik, Watchtower, and CI/CD agents often request socket access to discover services or build images, giving an application raw access to &lt;code&gt;/var/run/docker.sock&lt;/code&gt; is equivalent to handing it &lt;strong&gt;passwordless root privileges on the host operating system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this guide, we examine the mechanics of the 1-line Docker socket breakout, and deploy three battle-tested defenses to eliminate privilege escalation risks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Threat &amp;amp; Multi-Layer Defense Architecture
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph DangerZone["Vulnerability: Exposed /var/run/docker.sock"]
        VulnContainer["Compromised Container / Web App"]
        RawSocket["Direct /var/run/docker.sock Mount&amp;lt;br/&amp;gt;(Root-Equivalent Daemon API)"]
        HostTakeover["Container Escape &amp;amp; Host Takeover&amp;lt;br/&amp;gt;docker run -v /:/host alpine chroot /host&amp;lt;br/&amp;gt;(Complete Root Compromise)"]
    end

    subgraph DefenseLayer["Multi-Layer Socket Defense Architecture"]
        subgraph Proxy["Defense 1: Docker Socket Proxy (Tecnativa)"]
            SocketProxy["HAProxy Security Gateway&amp;lt;br/&amp;gt;CONTAINERS=1, POST=0, EXEC=0"]
            SafeAgent["Monitoring Tools (Portainer, Traefik)&amp;lt;br/&amp;gt;(Restricted to Read-Only GET)"]
        end

        subgraph Rootless["Defense 2: Rootless Docker Engine"]
            RootlessDaemon["Rootless dockerd (User Namespaces)&amp;lt;br/&amp;gt;Daemon runs as UID 1000"]
            HostSafety["Host Kernel Security&amp;lt;br/&amp;gt;(Escapes drop to unprivileged user)"]
        end

        subgraph TLSAuth["Defense 3: Mutual TLS (mTLS)"]
            CertAuth["TCP 2376 with Client Certificate&amp;lt;br/&amp;gt;(Replaces raw unencrypted TCP 2375)"]
        end
    end

    VulnContainer --&amp;gt;|Direct mounting| RawSocket
    RawSocket --&amp;gt;|Arbitrary container creation| HostTakeover

    SafeAgent --&amp;gt; SocketProxy
    SocketProxy --&amp;gt;|Filtered Safe API Calls| RawSocket
    SocketProxy -.-&amp;gt;|Blocks POST /containers/create &amp;amp; /exec| RawSocket

    RootlessDaemon --&amp;gt; HostSafety
    CertAuth --&amp;gt; RootlessDaemon&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  1. The Threat: The 1-Line Host Escape Exploit
&lt;/h2&gt;

&lt;p&gt;The Docker daemon (&lt;code&gt;dockerd&lt;/code&gt;) executes as host &lt;code&gt;root&lt;/code&gt; (&lt;code&gt;UID 0&lt;/code&gt;). The socket &lt;code&gt;/var/run/docker.sock&lt;/code&gt; is the Unix IPC bridge to that daemon.&lt;/p&gt;

&lt;p&gt;If a web application has a remote code execution (RCE) or Server-Side Request Forgery (SSRF) flaw and has &lt;code&gt;/var/run/docker.sock&lt;/code&gt; mounted, an attacker does not need an unpatched Linux kernel vulnerability to take over the host.&lt;/p&gt;

&lt;p&gt;They can issue a single command via the socket:&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;# Executed by an attacker inside the compromised container:&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /:/host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;chroot&lt;/span&gt; /host /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Read-Only (&lt;code&gt;:ro&lt;/code&gt;) Fails
&lt;/h3&gt;

&lt;p&gt;Many teams believe mounting the socket with &lt;code&gt;:ro&lt;/code&gt; protects them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock:ro my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This does not stop an attack.&lt;/strong&gt; The &lt;code&gt;:ro&lt;/code&gt; flag prevents modifying the Unix socket file inode on disk. It does &lt;strong&gt;not&lt;/strong&gt; stop processes from writing HTTP &lt;code&gt;POST&lt;/code&gt; requests into the socket stream to instruct the daemon to launch new containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Defense 1: Deploying Tecnativa Docker Socket Proxy
&lt;/h2&gt;

&lt;p&gt;When dashboard and monitoring tools need Docker metrics, place &lt;strong&gt;Tecnativa's Docker Socket Proxy&lt;/strong&gt; in front of the daemon. This hardened HAProxy image inspects API paths and enforces strict HTTP verb filtering:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;socket-proxy&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;tecnativa/docker-socket-proxy:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-socket-proxy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Block all state-mutating verbs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POST=0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DELETE=0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXEC=0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BUILD=0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;VOLUMES=0&lt;/span&gt;
      &lt;span class="c1"&gt;# Allow strictly read-only inspection&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CONTAINERS=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SERVICES=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NETWORKS=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;VERSION=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;INFO=1&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;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mgmt-net&lt;/span&gt;

  &lt;span class="na"&gt;traefik&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;traefik:v3.0&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;command&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;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.endpoint=tcp://socket-proxy:2375"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;socket-proxy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mgmt-net&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web-net&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mgmt-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# No external egress&lt;/span&gt;
  &lt;span class="na"&gt;web-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker tries to call &lt;code&gt;POST /containers/create&lt;/code&gt; through this proxy, HAProxy immediately denies the request with an &lt;code&gt;HTTP 403 Forbidden&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Defense 2: Migrating to Rootless Docker
&lt;/h2&gt;

&lt;p&gt;Rootless Docker runs both &lt;code&gt;dockerd&lt;/code&gt; and container runtimes entirely inside a user namespace (&lt;code&gt;userns&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In standard Docker: Container root (&lt;code&gt;UID 0&lt;/code&gt;) is host kernel &lt;code&gt;UID 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In Rootless Docker: Container root (&lt;code&gt;UID 0&lt;/code&gt;) maps to an unprivileged host user (&lt;code&gt;UID 100000+&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if a malicious actor escapes a container, &lt;strong&gt;they land on the host as an unprivileged user without &lt;code&gt;sudo&lt;/code&gt; or raw hardware privileges.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Setup on Ubuntu:
&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;# 1. Install prerequisites&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; uidmap dbus-user-session fuse-overlayfs

&lt;span class="c"&gt;# 2. Disable system daemon&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; docker.service docker.socket

&lt;span class="c"&gt;# 3. Install rootless daemon for your user&lt;/span&gt;
dockerd-rootless-setuptool.sh &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 4. Point Docker CLI to user socket&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:///run/user/&lt;span class="nv"&gt;$UID&lt;/span&gt;/docker.sock
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export DOCKER_HOST=unix:///run/user/$UID/docker.sock'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Defense 3: Remote Socket Security with Mutual TLS (mTLS)
&lt;/h2&gt;

&lt;p&gt;If you must manage Docker across the network, &lt;strong&gt;never bind raw TCP port 2375&lt;/strong&gt;. Anyone with local network access can execute arbitrary commands.&lt;/p&gt;

&lt;p&gt;Instead, configure &lt;strong&gt;Mutual TLS (mTLS)&lt;/strong&gt; on port &lt;code&gt;2376&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;/etc/docker/daemon.json&lt;/code&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;"tls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tlsverify"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tlscacert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/docker/ca.pem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tlscert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/docker/server-cert.pem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tlskey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/docker/server-key.pem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hosts"&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="s2"&gt;"fd://"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp://0.0.0.0:2376"&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;Clients must present valid cryptographic certificates signed by your private CA to establish connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--tlsverify&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tlscacert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tlscert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tlskey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;key.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp://docker.example.com:2376 ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Audit Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protection Layer&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Threat Prevented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Firewall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tecnativa Socket Proxy (&lt;code&gt;POST=0&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Host takeover via container mounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Namespace Isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rootless Docker (&lt;code&gt;dockerd-rootless&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Root filesystem breakout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Encrypted Transit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Port 2376 with mTLS (Never 2375)&lt;/td&gt;
&lt;td&gt;Unauthenticated remote execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS Auditing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auditctl -w /var/run/docker.sock -p wa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Silent unauthorized socket access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gearflowlab.com/secure-docker-daemon-socket-privilege-escalation/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>How to Fix Nginx 502 Bad Gateway with Docker (Resolving Upstream Connection Refused)</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:29:58 +0000</pubDate>
      <link>https://dev.to/gearflowlab/how-to-fix-nginx-502-bad-gateway-with-docker-resolving-upstream-connection-refused-47l8</link>
      <guid>https://dev.to/gearflowlab/how-to-fix-nginx-502-bad-gateway-with-docker-resolving-upstream-connection-refused-47l8</guid>
      <description>&lt;p&gt;An &lt;code&gt;HTTP 502 Bad Gateway&lt;/code&gt; error in an Nginx + Docker architecture is one of the most common—and frustrating—production roadblocks. Unlike a &lt;code&gt;504 Gateway Timeout&lt;/code&gt; where an upstream server is simply slow, a &lt;strong&gt;502 Bad Gateway&lt;/strong&gt; means Nginx is actively unable to complete the TCP handshake with your upstream container or received an invalid response header.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll break down the socket mechanics behind &lt;code&gt;connect() failed (111: Connection refused)&lt;/code&gt;, examine the three most common architectural misconfigurations, and implement bulletproof Docker DNS resolvers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The System Architecture &amp;amp; Failure Flow
&lt;/h2&gt;

&lt;p&gt;Here is how traffic flows through Nginx and Docker, and where the connection refused error is triggered:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph Client["1. External Traffic Layer"]
        Browser["Web Client / API Request&amp;lt;br/&amp;gt;(HTTPS GET / POST)"]
    end

    subgraph NginxProxy["2. Nginx Reverse Proxy Container"]
        NginxListener["Nginx Listener (Port 80 / 443)"]
        NginxDNS["Nginx Dynamic Resolver&amp;lt;br/&amp;gt;(resolver 127.0.0.11 valid=10s)"]
        ProxyPass["proxy_pass $upstream_app&amp;lt;br/&amp;gt;(Prevents startup IP caching trap)"]
    end

    subgraph DockerNetwork["3. Shared Docker Bridge Network (app-tier)"]
        DockerDNS["Docker Embedded DNS&amp;lt;br/&amp;gt;(127.0.0.11:53)"]

        subgraph BackendApp["Backend Application Container"]
            PortListen["Bound to 0.0.0.0:3000&amp;lt;br/&amp;gt;(Container Interface)"]
            AppLogic["Node.js / Python / Go Server"]
        end

        subgraph StaleTrap["Failure Scenario: 502 Bad Gateway"]
            DeadSocket["connect() failed (111: Connection refused)&amp;lt;br/&amp;gt;- App bound to 127.0.0.1 only&amp;lt;br/&amp;gt;- Unshared Docker network&amp;lt;br/&amp;gt;- Stale IP after container recreate"]
        end
    end

    Browser --&amp;gt;|1. Incoming Request| NginxListener
    NginxListener --&amp;gt; ProxyPass
    ProxyPass --&amp;gt;|2. Resolve container hostname| NginxDNS
    NginxDNS --&amp;gt;|3. Query 127.0.0.11| DockerDNS
    DockerDNS --&amp;gt;|4. Return active container IP| NginxDNS
    ProxyPass --&amp;gt;|5. Clean TCP Stream| PortListen
    PortListen --&amp;gt; AppLogic
    ProxyPass -.-&amp;gt;|Misconfiguration| DeadSocket&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  1. The Anatomy of POSIX Error 111 (Connection Refused)
&lt;/h2&gt;

&lt;p&gt;When you inspect Nginx container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &amp;lt;nginx_container&amp;gt; &lt;span class="nt"&gt;--tail&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will almost always find this exact signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026/10/10 05:14:22 [error] 28#28: *104 connect() failed (111: Connection refused) 
while connecting to upstream, client: 198.51.100.45, server: api.gearflowlab.com, 
request: "POST /v1/auth/login HTTP/1.1", upstream: "http://172.22.0.4:8080/v1/auth/login", 
host: "api.gearflowlab.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In POSIX socket networking, &lt;code&gt;111&lt;/code&gt; means the target host responded with an immediate &lt;strong&gt;TCP RST (Reset)&lt;/strong&gt; flag. Nginx sent a &lt;code&gt;SYN&lt;/code&gt; packet to establish a TCP session, but the operating system at &lt;code&gt;172.22.0.4&lt;/code&gt; replied: &lt;em&gt;"No process is listening on port 8080."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Root Cause 1: Binding to &lt;code&gt;127.0.0.1&lt;/code&gt; Inside Containers
&lt;/h2&gt;

&lt;p&gt;The #1 reason for a 502 Bad Gateway in Docker: your application server binds to &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Docker, &lt;strong&gt;every container has an isolated network namespace and its own loopback interface (&lt;code&gt;lo&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When your app listens on &lt;code&gt;127.0.0.1:3000&lt;/code&gt;, it is only accessible from &lt;em&gt;inside that same container&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Nginx connects via the Docker bridge network interface (&lt;code&gt;eth0&lt;/code&gt;, e.g., &lt;code&gt;172.22.0.4&lt;/code&gt;). Because your app is not listening on &lt;code&gt;eth0&lt;/code&gt;, the kernel drops the packet with &lt;code&gt;Connection refused&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: Bind to &lt;code&gt;0.0.0.0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In Node.js / Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Correct: Listen on all network interfaces&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&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="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 listening on 0.0.0.0:&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;p&gt;In Python FastAPI / Uvicorn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Root Cause 2: The Stale IP Dynamic DNS Caching Trap
&lt;/h2&gt;

&lt;p&gt;This is the silent killer of container deployments.&lt;/p&gt;

&lt;p&gt;When you define a static &lt;code&gt;proxy_pass&lt;/code&gt; in Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend-api:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx queries DNS for &lt;code&gt;backend-api&lt;/code&gt; &lt;strong&gt;once during boot or reload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you subsequently update your backend service (&lt;code&gt;docker compose up -d --no-deps backend-api&lt;/code&gt;), Docker destroys the old container and creates a new container with a &lt;strong&gt;new IP address&lt;/strong&gt; (e.g., &lt;code&gt;172.22.0.4&lt;/code&gt; becomes &lt;code&gt;172.22.0.5&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Nginx continues sending packets to &lt;code&gt;172.22.0.4&lt;/code&gt; until you reload Nginx. Every request fails with &lt;code&gt;502 Bad Gateway (Connection refused)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Variable &lt;code&gt;proxy_pass&lt;/code&gt; with Docker DNS
&lt;/h3&gt;

&lt;p&gt;By using an Nginx variable inside &lt;code&gt;proxy_pass&lt;/code&gt; and configuring Nginx's &lt;code&gt;resolver&lt;/code&gt; to point to Docker's internal DNS daemon (&lt;code&gt;127.0.0.11&lt;/code&gt;), Nginx dynamically re-resolves the IP every few seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.gearflowlab.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# 127.0.0.11 is Docker's embedded DNS server&lt;/span&gt;
    &lt;span class="kn"&gt;resolver&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="s"&gt;.0.11&lt;/span&gt; &lt;span class="s"&gt;valid=5s&lt;/span&gt; &lt;span class="s"&gt;ipv6=off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;resolver_timeout&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Storing in a variable forces runtime DNS resolution&lt;/span&gt;
    &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$backend_service&lt;/span&gt; &lt;span class="s"&gt;"http://backend-api:3000"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="nv"&gt;$backend_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Retry on transient rollout hiccups&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_next_upstream&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="s"&gt;http_502&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_next_upstream_tries&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Root Cause 3: Default Bridge vs. User-Defined Bridge Networks
&lt;/h2&gt;

&lt;p&gt;Containers on Docker's default &lt;code&gt;bridge&lt;/code&gt; network &lt;strong&gt;do not support automatic DNS resolution by container name&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Always ensure both services share a named, user-defined bridge network in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reverse-proxy&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;nginx: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;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&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;./nginx.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-net&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;backend-api&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;backend-api&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;my-node-api:latest&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;HOST=0.0.0.0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-net&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;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-qO-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:3000/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&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;3s&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;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;condition: service_healthy&lt;/code&gt; directive: Nginx will not accept traffic until the backend passes its internal healthcheck, eliminating startup race condition 502s.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnostic Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;Run these commands inside your environment to pinpoint the root cause in 30 seconds:&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;# 1. Test DNS resolution from Nginx container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; reverse-proxy getent hosts backend-api

&lt;span class="c"&gt;# 2. Test HTTP response directly from Nginx container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; reverse-proxy wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; http://backend-api:3000/health

&lt;span class="c"&gt;# 3. Check what port the app is listening on inside its container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; backend-api netstat &lt;span class="nt"&gt;-tulpn&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;LISTEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gearflowlab.com/nginx-reverse-proxy-docker-502-bad-gateway/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>nginx</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fix n8n Webhook 413 Payload Too Large on Docker &amp; Nginx (The 3-Layer Solution)</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 03:57:33 +0000</pubDate>
      <link>https://dev.to/gearflowlab/fix-n8n-webhook-413-payload-too-large-on-docker-nginx-the-3-layer-solution-5chg</link>
      <guid>https://dev.to/gearflowlab/fix-n8n-webhook-413-payload-too-large-on-docker-nginx-the-3-layer-solution-5chg</guid>
      <description>&lt;p&gt;You’re self-hosting n8n on Docker behind an Nginx reverse proxy. Your workflows run flawlessly with small test payloads and test webhooks.&lt;/p&gt;

&lt;p&gt;Then, a real production request hits: a Stripe webhook carrying a heavy metadata object, an external form pushing a 25MB PDF attachment, or a web scraper dispatching a large JSON dataset. &lt;/p&gt;

&lt;p&gt;Suddenly, the sender receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 413 Request Entity Too Large
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or inside your Docker logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PayloadTooLargeError: request entity too large
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fn8n-webhook-413-error-postman.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/images%2Fn8n-webhook-413-error-postman.png" alt="n8n Webhook 413 Payload Too Large Error" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Screenshot of Postman, Insomnia, or a dark terminal sending a POST request with a ~20MB file returning &lt;code&gt;413 Request Entity Too Large&lt;/code&gt; and an HTML body containing &lt;code&gt;&amp;lt;center&amp;gt;nginx&amp;lt;/center&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "A realistic screenshot of a dark-mode developer API client (Postman or Insomnia) showing a failed POST request to an n8n webhook endpoint with an HTTP 413 Payload Too Large error status badge in red, raw response body showing HTML header, ultra-clean software engineer UI, 4k."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most infuriating part? &lt;strong&gt;The workflow execution log in n8n is completely blank.&lt;/strong&gt; The request was dropped before the Webhook node ever received it.&lt;/p&gt;

&lt;p&gt;Most quick forum posts say &lt;em&gt;"just add &lt;code&gt;client_max_body_size 64M;&lt;/code&gt; to Nginx."&lt;/em&gt; But if you only do that, you'll still hit a wall. In real-world self-hosting, there are &lt;strong&gt;three distinct layers&lt;/strong&gt; that enforce payload limits, plus a hidden Node.js V8 memory heap trap.&lt;/p&gt;

&lt;p&gt;Here is the complete engineering walkthrough to identify which layer is rejecting your request and the configuration required to fix all of them permanently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Webhook Chain: Three Layers, Three Limits
&lt;/h2&gt;

&lt;p&gt;A webhook request passes through a multi-tiered pipeline before reaching your n8n canvas:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Client["Webhook Sender&amp;lt;br/&amp;gt;(Stripe, Web Scraper, Form)"]
    CF["Cloudflare Proxy&amp;lt;br/&amp;gt;(100MB Cap on Free/Pro)"]
    Nginx["Nginx Reverse Proxy&amp;lt;br/&amp;gt;(client_max_body_size: 1MB Default)"]
    n8n["n8n Express Server&amp;lt;br/&amp;gt;(N8N_PAYLOAD_SIZE_MAX: 16MiB Default)"]
    V8["Node.js V8 Heap&amp;lt;br/&amp;gt;(max-old-space-size)"]
    Node["Webhook Node Execution&amp;lt;br/&amp;gt;(n8n Canvas Workflow)"]

    Client --&amp;gt; CF
    CF -- "&amp;lt; 100MB" --&amp;gt; Nginx
    Nginx -- "&amp;lt; 64MB" --&amp;gt; n8n
    n8n --&amp;gt; V8
    V8 --&amp;gt; Node&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each layer enforces its own payload cap, and &lt;strong&gt;the smallest limit always wins&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;Layer&lt;/th&gt;
&lt;th&gt;Directive / Setting&lt;/th&gt;
&lt;th&gt;Default Limit&lt;/th&gt;
&lt;th&gt;Error Signature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nginx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;client_max_body_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML page: &lt;code&gt;&amp;lt;title&amp;gt;413 Request Entity Too Large&amp;lt;/title&amp;gt;...&amp;lt;center&amp;gt;nginx&amp;lt;/center&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;n8n (JSON / Body)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N8N_PAYLOAD_SIZE_MAX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PayloadTooLargeError: request entity too large&lt;/code&gt; in Docker logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;n8n (Multipart File)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N8N_FORMDATA_FILE_SIZE_MAX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Upload rejected by busboy parser in container logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Plan-based upload limit&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100 MB&lt;/strong&gt; (Free/Pro)&lt;/td&gt;
&lt;td&gt;Cloudflare branded error page, &lt;code&gt;Server: cloudflare&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1: Isolate Which Layer is Rejecting the Payload
&lt;/h2&gt;

&lt;p&gt;Before editing configs, determine exactly where the payload is dropped:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Send a 20 MB test payload through your public URL (full chain):&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json; print(json.dumps({'data': 'x' * 20_000_000}))"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; payload.json

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://n8n.yourdomain.com/webhook-test/your-path"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @payload.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Send the identical payload directly to n8n from the host machine (bypassing Nginx):&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://127.0.0.1:5678/webhook-test/your-path"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @payload.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to diagnose the response:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public URL returns 413 with &lt;code&gt;&amp;lt;center&amp;gt;nginx&amp;lt;/center&amp;gt;&lt;/code&gt;, but local 127.0.0.1 succeeds:&lt;/strong&gt; Nginx is the culprit. Proceed to &lt;strong&gt;Step 2&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct curl to 127.0.0.1 returns 413 or logs &lt;code&gt;PayloadTooLargeError&lt;/code&gt;:&lt;/strong&gt; n8n itself rejected it. Proceed to &lt;strong&gt;Step 3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public URL returns &lt;code&gt;Server: cloudflare&lt;/code&gt;:&lt;/strong&gt; Cloudflare's edge proxy rejected it before reaching your origin.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 2: Raise Nginx Limits &amp;amp; Adjust Timeouts
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;/etc/nginx/sites-available/n8n&lt;/code&gt; (or &lt;code&gt;/etc/nginx/conf.d/n8n.conf&lt;/code&gt;), scope the body size to your webhook endpoints so your administrative editor UI remains strictly protected:&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/images%2Fnginx-n8n-client-max-body-size-config.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/images%2Fnginx-n8n-client-max-body-size-config.png" alt="Nginx Configuration for n8n Webhook" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Screenshot of VS Code or Neovim with Nginx syntax highlighting on a Linux server, showing the &lt;code&gt;location ~ ^/(webhook|webhook-test)/&lt;/code&gt; block with &lt;code&gt;client_max_body_size 64m;&lt;/code&gt; highlighted in line numbers.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "High-resolution screenshot of a code editor with a dark modern theme (One Dark / Dracula) displaying an nginx configuration file, highlighting &lt;code&gt;client_max_body_size 64m;&lt;/code&gt; and &lt;code&gt;proxy_read_timeout 300s;&lt;/code&gt; within a location block, syntax-highlighted Nginx directives."
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;n8n.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# General protection for the editor UI &amp;amp; API&lt;/span&gt;
    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;4m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Dedicated block for incoming Webhook payloads&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/(webhook|webhook-test)/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;64m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Prevent 504 Gateway Timeouts while uploading large payloads&lt;/span&gt;
        &lt;span class="kn"&gt;client_body_timeout&lt;/span&gt; &lt;span class="s"&gt;120s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt;  &lt;span class="s"&gt;300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_send_timeout&lt;/span&gt;  &lt;span class="s"&gt;300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Disable buffering so large uploads stream directly to n8n&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_request_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Main editor &amp;amp; websocket block&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the syntax and reload Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Tune n8n Environment Variables &amp;amp; Node.js V8 Heap
&lt;/h2&gt;

&lt;p&gt;n8n parses JSON bodies using its internal Express body-parser middleware. By default, &lt;code&gt;N8N_PAYLOAD_SIZE_MAX&lt;/code&gt; is capped at &lt;strong&gt;16 MiB&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Furthermore, if you accept a 50 MB JSON payload, Node.js will parse that entire object tree into the V8 memory heap. By default, Node.js inside the official Alpine container runs with limited heap memory (~1.4 GB) and will crash with &lt;code&gt;JavaScript heap out of memory&lt;/code&gt;.&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/images%2Fn8n-docker-compose-payload-env.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/images%2Fn8n-docker-compose-payload-env.png" alt="n8n Docker Compose Environment Settings" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; A split-screen terminal showing &lt;code&gt;docker-compose.yml&lt;/code&gt; on the left and &lt;code&gt;docker exec n8n printenv | grep N8N&lt;/code&gt; on the right confirming the 64M payload limit and 4096MB heap allocation.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "A developer workstation split-screen terminal in dark mode, showing a clean docker-compose.yml configuration with environment variables &lt;code&gt;N8N_PAYLOAD_SIZE_MAX=64&lt;/code&gt; and &lt;code&gt;NODE_OPTIONS=--max-old-space-size=4096&lt;/code&gt;, modern terminal font (JetBrains Mono), clean contrast."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Update your &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&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;docker.n8n.io/n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# 1. Raise n8n JSON / raw body limit (in MiB)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PAYLOAD_SIZE_MAX=64&lt;/span&gt;

      &lt;span class="c1"&gt;# 2. Raise multipart/form-data binary upload limit (in MiB)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_FORMDATA_FILE_SIZE_MAX=250&lt;/span&gt;

      &lt;span class="c1"&gt;# 3. CRITICAL: Store binary files on disk instead of holding them in RAM&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_DEFAULT_BINARY_DATA_MODE=filesystem&lt;/span&gt;

      &lt;span class="c1"&gt;# 4. Prevent Node.js V8 Heap OOM crashes on large JSON parsing (allocate 4GB heap)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_OPTIONS=--max-old-space-size=4096&lt;/span&gt;

      &lt;span class="c1"&gt;# Generic timezone settings&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=UTC&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=UTC&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;n8n_data:/home/node/.n8n&lt;/span&gt;
      &lt;span class="c1"&gt;# If using binary data mode filesystem, ensure local mount persists:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./n8n-local-files:/files&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;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Running &lt;code&gt;docker restart n8n&lt;/code&gt; does &lt;strong&gt;NOT&lt;/strong&gt; apply new environment variables. You must recreate the container:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that the variables are active inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;n8n &lt;span class="nb"&gt;printenv&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"N8N_PAYLOAD_SIZE_MAX|NODE_OPTIONS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Verification &amp;amp; End-to-End Test
&lt;/h2&gt;

&lt;p&gt;Re-run your 20MB payload test through your public domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"HTTP %{http_code} | sent %{size_upload} bytes | %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://n8n.yourdomain.com/webhook/your-path"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @payload.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fn8n-webhook-200-ok-success.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/images%2Fn8n-webhook-200-ok-success.png" alt="n8n Webhook Execution Success" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Screenshot of the n8n Workflow canvas with the Webhook node glowing green with a checkmark ("Executed successfully"), showing an input payload of 20MB and a status badge of 200 OK.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "UI screenshot of n8n automation canvas in dark mode, showing a Webhook node connected to a Code node, green success badges, node output inspector open showing a clean 200 OK response with a large JSON data payload."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200 | sent 20000013 bytes | 1.84s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your n8n workflow execution history: the execution now appears immediately, and the node outputs the full payload without dropping or choking the container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Architecture Best Practices
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep Nginx limit ≥ n8n limit:&lt;/strong&gt; If n8n allows 64 MiB but Nginx allows 32 MB, Nginx will drop the connection before n8n ever sees it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never inline massive binaries as base64 in JSON:&lt;/strong&gt; Base64 inflates payload size by ~33% and forces the V8 engine to allocate massive contiguous memory chunks. Always use &lt;code&gt;multipart/form-data&lt;/code&gt; with &lt;code&gt;N8N_DEFAULT_BINARY_DATA_MODE=filesystem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If behind Cloudflare:&lt;/strong&gt; Cloudflare Free and Pro plans hard-cap uploads at 100 MB. If you need payloads &amp;gt;100 MB, route webhooks through a DNS-only (grey-cloud) subdomain or have the client upload directly to S3/Cloudflare R2 and pass a pre-signed URL to n8n instead.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gearflowlab.com/fix-n8n-webhook-413-payload-too-large/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How to Fix WSL2 and Docker (vmmem) Eating 90% of Your RAM on Windows 11</title>
      <dc:creator>Sanghun Yun</dc:creator>
      <pubDate>Sat, 10 Oct 2026 03:51:52 +0000</pubDate>
      <link>https://dev.to/gearflowlab/how-to-fix-wsl2-and-docker-vmmem-eating-90-of-your-ram-on-windows-11-2gja</link>
      <guid>https://dev.to/gearflowlab/how-to-fix-wsl2-and-docker-vmmem-eating-90-of-your-ram-on-windows-11-2gja</guid>
      <description>&lt;p&gt;You’re running Windows 11 as your daily driver for software engineering. You launch Docker Desktop, spin up your local dev containers (Postgres, Redis, an API server), and start writing code. Everything feels snappy.&lt;/p&gt;

&lt;p&gt;A few hours into your workday, your entire system starts stuttering. Browser tabs crash or constantly reload, typing inside VS Code starts lagging, and your laptop fans sound like a jet engine preparing for takeoff.&lt;/p&gt;

&lt;p&gt;You open &lt;strong&gt;Windows Task Manager&lt;/strong&gt; (&lt;code&gt;Ctrl + Shift + Esc&lt;/code&gt;), sort by &lt;strong&gt;Memory&lt;/strong&gt;, and discover this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process Name           PID       Status       CPU       Memory (Private Working Set)
vmmemWSL (or vmmem)    4128      Running      1.5%      24,180.4 MB (88%)
Docker Desktop         11452     Running      0.1%         412.0 MB
Windows Explorer       2840      Running      0.0%         185.3 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fdevto-task-manager-vmmem-high-memory.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/images%2Fdevto-task-manager-vmmem-high-memory.png" alt="Task Manager showing vmmemWSL consuming 24GB of RAM" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Capture the Windows 11 Task Manager Processes tab sorted by Memory descending, showing &lt;code&gt;vmmemWSL&lt;/code&gt; consuming over 24 GB of RAM (80–90% total system memory), with high system memory gauge in red.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "High-resolution desktop UI screenshot of Windows 11 Task Manager in dark mode, showing the 'Processes' tab sorted by Memory, with 'vmmemWSL' highlighted consuming 24.2 GB of RAM, memory utilization bar graph showing 91% total usage, clean authentic Windows 11 design language."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even after you run &lt;code&gt;docker stop $(docker ps -q)&lt;/code&gt;, terminate all active terminal sessions, and close VS Code, &lt;strong&gt;&lt;code&gt;vmmemWSL&lt;/code&gt; continues holding 20+ GB of RAM hostage.&lt;/strong&gt; Windows 11 never reclaims it.&lt;/p&gt;

&lt;p&gt;Here is why this happens, how WSL2's memory model actually works, and the step-by-step configuration to permanently tame &lt;code&gt;vmmem&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause: Why Doesn't Linux Give Memory Back to Windows?
&lt;/h2&gt;

&lt;p&gt;To understand this problem, you need to know how WSL2 works under the hood.&lt;/p&gt;

&lt;p&gt;WSL2 is not a container engine or an emulation layer; it runs a &lt;strong&gt;real Linux kernel inside a lightweight Hyper-V utility virtual machine&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Unlike traditional virtualization platforms where you assign a static block of RAM (e.g., exactly 8 GB), Hyper-V uses &lt;strong&gt;Dynamic Memory Allocation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph Host["Windows 11 Host OS (e.g. 32 GB RAM)"]
        subgraph HyperV["Hyper-V Dynamic Memory Manager"]
            VM["vmmemWSL Virtual Machine Allocation&amp;lt;br/&amp;gt;(Expands up to 50% - 80% Total RAM by default)"]
            subgraph WSL2["WSL2 Linux Kernel Guest"]
                AppMem["Active App / Container RAM&amp;lt;br/&amp;gt;(e.g. 2 - 4 GB)"]
                PageCache["Linux Page Cache &amp;amp; Buffers&amp;lt;br/&amp;gt;(Files cached during docker build, npm, git)&amp;lt;br/&amp;gt;(Inflates to 18+ GB)"]
            end
        end
        WinApps["Windows Host Processes&amp;lt;br/&amp;gt;(Chrome, VS Code, OS Services)&amp;lt;br/&amp;gt;(Starved when vmmem expands)"]
    end

    PageCache -- "1. Reads files, inflates memory" --&amp;gt; PageCache
    WSL2 -- "2. Requests memory expansion" --&amp;gt; VM
    VM -- "3. Hyper-V allocates host RAM" --&amp;gt; Host
    Host -. "4. Linux DOES NOT return idle cache&amp;lt;br/&amp;gt;to host without .wslconfig caps" .-&amp;gt; WinApps&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. The Page Cache Trap
&lt;/h3&gt;

&lt;p&gt;The Linux kernel operates on a fundamental principle: &lt;strong&gt;"Free RAM is wasted RAM."&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;When you build a Docker image (&lt;code&gt;docker build&lt;/code&gt;), run package managers (&lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;pip install&lt;/code&gt;), or perform Git operations on large repositories, Linux reads and writes thousands of files. Linux automatically keeps every single file it reads cached in memory (in buffers, dentries, and inodes) so subsequent access is instantaneous.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hyper-V Ballooning
&lt;/h3&gt;

&lt;p&gt;Hyper-V detects the Linux kernel asking for more memory to satisfy these caches and dutifully expands the host RAM assigned to &lt;code&gt;vmmemWSL&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Lack of Automatic Deflation
&lt;/h3&gt;

&lt;p&gt;When your build or container workload finishes, Linux marks that cached RAM as "available for other Linux processes," but &lt;strong&gt;it does not surrender it back to the Hyper-V host&lt;/strong&gt;. To Windows 11, that memory is still actively locked by the Hyper-V VM.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Overly Generous Defaults
&lt;/h3&gt;

&lt;p&gt;By default, WSL2 on Windows 11 is allowed to claim &lt;strong&gt;up to 50% of your total host RAM&lt;/strong&gt; (capped at 32 GB), and up to &lt;strong&gt;80%&lt;/strong&gt; on earlier Windows builds. On a 32 GB machine, WSL2 will happily balloon to 16-24 GB and never let go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Hard-Cap WSL2 in &lt;code&gt;%UserProfile%\.wslconfig&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The permanent solution is to define hard resource boundaries using a global &lt;code&gt;.wslconfig&lt;/code&gt; file in your Windows user profile directory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Win + R&lt;/code&gt;, type &lt;code&gt;notepad %UserProfile%\.wslconfig&lt;/code&gt;, and press &lt;strong&gt;Enter&lt;/strong&gt;. (If prompted to create the file, click &lt;strong&gt;Yes&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Paste the following configuration:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fdevto-wslconfig-configuration-guide.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/images%2Fdevto-wslconfig-configuration-guide.png" alt="Editing .wslconfig in VS Code" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Screenshot of VS Code or Notepad editing &lt;code&gt;.wslconfig&lt;/code&gt;, highlighting &lt;code&gt;memory=8GB&lt;/code&gt;, &lt;code&gt;processors=4&lt;/code&gt;, and &lt;code&gt;autoMemoryReclaim=gradual&lt;/code&gt; with line numbers visible.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "A modern dark-themed code editor window (VS Code) displaying a &lt;code&gt;.wslconfig&lt;/code&gt; configuration file, showing clean INI syntax highlighting with sections &lt;code&gt;[wsl2]&lt;/code&gt; and &lt;code&gt;[experimental]&lt;/code&gt;, highlighting &lt;code&gt;memory=8GB&lt;/code&gt; and &lt;code&gt;autoMemoryReclaim=gradual&lt;/code&gt; in golden accents, professional developer workstation capture."
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Settings apply globally to all WSL2 distributions &amp;amp; Docker Desktop backend
&lt;/span&gt;&lt;span class="nn"&gt;[wsl2]&lt;/span&gt;
&lt;span class="c"&gt;# Hard limit memory allocated to the WSL2 virtual machine
&lt;/span&gt;&lt;span class="py"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8GB&lt;/span&gt;

&lt;span class="c"&gt;# Limits virtual CPU cores allocated to WSL2 (leaves headroom for Windows)
&lt;/span&gt;&lt;span class="py"&gt;processors&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;

&lt;span class="c"&gt;# Sets the swap virtual disk size (prevents Linux OOM crashes during build spikes)
&lt;/span&gt;&lt;span class="py"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4GB&lt;/span&gt;

&lt;span class="c"&gt;# Windows 11 23H2+ Memory Reclaim feature:
# Automatically releases cached page cache memory back to Windows
&lt;/span&gt;&lt;span class="py"&gt;autoMemoryReclaim&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;gradual&lt;/span&gt;

&lt;span class="c"&gt;# Enable clean localhost forwarding from containers to Windows
&lt;/span&gt;&lt;span class="py"&gt;localhostForwarding&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[experimental]&lt;/span&gt;
&lt;span class="c"&gt;# Automatically drops Linux page caches when memory pressure drops
&lt;/span&gt;&lt;span class="py"&gt;autoMemoryReclaim&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dropcache&lt;/span&gt;
&lt;span class="c"&gt;# Enables automatic sparse disk compaction for virtual hard drives
&lt;/span&gt;&lt;span class="py"&gt;sparseVhd&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sizing Rule of Thumb for Your Hardware:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host Physical RAM&lt;/th&gt;
&lt;th&gt;Recommended &lt;code&gt;.wslconfig&lt;/code&gt; &lt;code&gt;memory&lt;/code&gt; Cap&lt;/th&gt;
&lt;th&gt;Recommended &lt;code&gt;swap&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;4GB&lt;/code&gt; or &lt;code&gt;6GB&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;32 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;8GB&lt;/code&gt; or &lt;code&gt;12GB&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;64 GB+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;16GB&lt;/code&gt; or &lt;code&gt;24GB&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 2: Restart WSL2 to Apply the Changes
&lt;/h2&gt;

&lt;p&gt;WSL2 only evaluates &lt;code&gt;.wslconfig&lt;/code&gt; during initial VM boot. Restarting Docker Desktop alone will &lt;strong&gt;not&lt;/strong&gt; reload &lt;code&gt;.wslconfig&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;PowerShell (Administrator)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Force-shutdown all running WSL2 instances and Hyper-V utility VMs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Look at Task Manager: &lt;code&gt;vmmemWSL&lt;/code&gt; will vanish immediately.&lt;/li&gt;
&lt;li&gt;Launch your WSL2 terminal or Docker Desktop. Hyper-V will now boot strictly constrained to your configured memory ceiling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify the active ceiling from inside your Linux terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fdevto-terminal-free-memory-verification.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/images%2Fdevto-terminal-free-memory-verification.png" alt="Linux Terminal free -h Verification" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Terminal screenshot of &lt;code&gt;free -h&lt;/code&gt; running in an Ubuntu WSL2 shell showing &lt;code&gt;total: 7.8Gi&lt;/code&gt;, &lt;code&gt;used: 1.2Gi&lt;/code&gt;, &lt;code&gt;free: 5.8Gi&lt;/code&gt;, and &lt;code&gt;Swap: 4.0Gi&lt;/code&gt;, displaying memory usage under full control.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "Terminal window in dark mode displaying the output of &lt;code&gt;free -h&lt;/code&gt; command in Linux, showing 7.8Gi total memory with 5.8Gi available and 4.0Gi swap space, clean monospace font, green terminal prompt, high contrast."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Output will now show your configured cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               total        used        free      shared  buff/cache   available
Mem:           7.8Gi       1.2Gi       5.8Gi       4.0Mi       840Mi       6.4Gi
Swap:          4.0Gi          0B       4.0Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Flush Cached RAM on Demand (Without Rebooting)
&lt;/h2&gt;

&lt;p&gt;If you are running back-to-back heavy builds and want to reclaim cached memory immediately without restarting your containers, you can manually trigger a kernel cache flush.&lt;/p&gt;

&lt;p&gt;Run this inside your WSL2 terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"sync; echo 3 &amp;gt; /proc/sys/vm/drop_caches"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this seamless, add a quick alias to your shell configuration (&lt;code&gt;~/.bashrc&lt;/code&gt; or &lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'alias dropmem="sudo sh -c \"sync; echo 3 &amp;gt; /proc/sys/vm/drop_caches\" &amp;amp;&amp;amp; free -h"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever RAM usage climbs, simply run &lt;code&gt;dropmem&lt;/code&gt; in any terminal to release gigabytes of cache back to your system in under 500ms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Reclaim Docker BuildKit Cache &amp;amp; Disk Space
&lt;/h2&gt;

&lt;p&gt;A huge hidden contributor to WSL2 memory and NVMe disk bloat is Docker's &lt;strong&gt;BuildKit layer cache&lt;/strong&gt;. Over weeks of development, BuildKit accumulates tens of gigabytes of cached layers inside WSL2's virtual disk (&lt;code&gt;ext4.vhdx&lt;/code&gt;).&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/images%2Fdevto-docker-build-cache-pruning.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/images%2Fdevto-docker-build-cache-pruning.png" alt="Docker BuildKit Cache Prune" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Image Guide / Prompt]&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Screenshot Capture:&lt;/em&gt; Terminal output running &lt;code&gt;docker builder prune -a -f&lt;/code&gt; showing deleted build cache IDs followed by &lt;code&gt;Total reclaimed space: 14.82GB&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AI Generation Prompt:&lt;/em&gt; "A sleek dark-mode terminal showing Docker CLI output for &lt;code&gt;docker builder prune -a -f&lt;/code&gt;, listing multiple purged layer hashes and concluding with &lt;code&gt;Total reclaimed space: 14.8GB&lt;/code&gt; highlighted in bold green."&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. Prune BuildKit Cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker builder prune &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Prune Dead Containers, Networks, and Dangling Images
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--volumes&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Compact the Expanding Virtual Disk (&lt;code&gt;ext4.vhdx&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Even after deleting 30 GB of Docker images, the &lt;code&gt;.vhdx&lt;/code&gt; file on your Windows drive does not shrink automatically. Run &lt;code&gt;diskpart&lt;/code&gt; in PowerShell to shrink it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;diskpart&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;diskpart&lt;/code&gt; prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select vdisk file="C:\Users\&amp;lt;YourUsername&amp;gt;\AppData\Local\Packages\CanonicalGroupLimited...\LocalState\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(For Docker Desktop data distro, target &lt;code&gt;%LOCALAPPDATA%\Docker\wsl\data\ext4.vhdx&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] Configure &lt;code&gt;%UserProfile%\.wslconfig&lt;/code&gt; with an explicit &lt;code&gt;memory&lt;/code&gt; and &lt;code&gt;swap&lt;/code&gt; cap.&lt;/li&gt;
&lt;li&gt;[x] Enable &lt;code&gt;autoMemoryReclaim=gradual&lt;/code&gt; to return idle page cache to Windows automatically.&lt;/li&gt;
&lt;li&gt;[x] Run &lt;code&gt;wsl --shutdown&lt;/code&gt; from PowerShell to apply settings.&lt;/li&gt;
&lt;li&gt;[x] Use &lt;code&gt;dropmem&lt;/code&gt; alias for instant manual cache purging.&lt;/li&gt;
&lt;li&gt;[x] Regularly prune Docker's build cache using &lt;code&gt;docker builder prune -a&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gearflowlab.com/wsl2-vmmem-high-memory-windows-11-fix/" rel="noopener noreferrer"&gt;GearFlow Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>windows</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
