<?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: Vpsfordev</title>
    <description>The latest articles on DEV Community by Vpsfordev (@vpsfordev).</description>
    <link>https://dev.to/vpsfordev</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%2F3963036%2F6dd5f64a-a79d-4f47-b644-7815cafb4604.png</url>
      <title>DEV Community: Vpsfordev</title>
      <link>https://dev.to/vpsfordev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vpsfordev"/>
    <language>en</language>
    <item>
      <title>I set up a Node.js production server on Hetzner CX22 for €4.35/mo — here's exactly what I ran</title>
      <dc:creator>Vpsfordev</dc:creator>
      <pubDate>Fri, 19 Jun 2026 14:51:18 +0000</pubDate>
      <link>https://dev.to/vpsfordev/i-set-up-a-nodejs-production-server-on-hetzner-cx22-for-eu435mo-heres-exactly-what-i-ran-2gp8</link>
      <guid>https://dev.to/vpsfordev/i-set-up-a-nodejs-production-server-on-hetzner-cx22-for-eu435mo-heres-exactly-what-i-ran-2gp8</guid>
      <description>&lt;p&gt;Most Node.js hosting guides assume you're using DigitalOcean or AWS. I switched to Hetzner CX22 (€4.35/mo, 2 vCPU, 4GB RAM) and it's been running production workloads for months without a single issue.&lt;/p&gt;

&lt;p&gt;Here's the exact setup I use — no fluff, just the commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Hetzner CX22
&lt;/h2&gt;

&lt;p&gt;A 4GB DigitalOcean droplet costs ~$24/month. The Hetzner CX22 with identical specs costs &lt;strong&gt;€4.35/month&lt;/strong&gt;. I ran &lt;a href="https://vpsfor.dev/posts/hetzner-vs-digitalocean-nodejs-benchmarks-2026/" rel="noopener noreferrer"&gt;benchmarks with autocannon&lt;/a&gt; — Hetzner is actually ~3.5% faster. So it's cheaper AND faster. The only trade-off is ticket-only support.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup (in order)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create a non-root user immediately
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Login as root (first and only time)&lt;/span&gt;
ssh root@YOUR_SERVER_IP

adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
rsync &lt;span class="nt"&gt;--archive&lt;/span&gt; &lt;span class="nt"&gt;--chown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy:deploy ~/.ssh /home/deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never run Node.js as root. This takes 30 seconds and saves you from a class of security issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Firewall on, then forget about it
&lt;/h3&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;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ufw &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&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;All ports closed except 22, 80, 443. Hetzner servers are fully open by default — this step is not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Node.js via nvm (not apt)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
nvm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--lts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apt install nodejs&lt;/code&gt; gives you an outdated version. nvm gives you the current LTS without sudo.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Swap file before anything else
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;On a 4GB VPS, &lt;code&gt;npm install&lt;/code&gt; during a deploy can spike RAM by 30-50%. Without swap, it OOM-kills your running app mid-deploy. I learned this the hard way.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. PM2 with memory limits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ecosystem.config.js&lt;/code&gt; for a 2-core server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// one per core&lt;/span&gt;
    &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;350M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;node_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--max-old-space-size=320&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env_production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;The &lt;code&gt;--max-old-space-size&lt;/code&gt; flag is critical. Without it, V8 can claim up to 1.5GB on a 4GB machine and crash the server. Two workers × 350MB = 700MB max, leaving ~90MB headroom for the OS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start ecosystem.config.js &lt;span class="nt"&gt;--env&lt;/span&gt; production
pm2 save
pm2 startup  &lt;span class="c"&gt;# run the command it prints&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Nginx + free SSL
&lt;/h3&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/nginx/sites-available/my-app&lt;/code&gt;:&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;yourdomain.com&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="s"&gt;http://localhost:3000&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="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_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/
&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;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot handles HTTPS config and sets up auto-renewal. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total time
&lt;/h2&gt;

&lt;p&gt;From fresh server to HTTPS Node.js app: &lt;strong&gt;~25 minutes&lt;/strong&gt; the first time, ~10 minutes once you know the commands.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set up SSH key auth only&lt;/strong&gt; (&lt;code&gt;PasswordAuthentication no&lt;/code&gt; in sshd_config) — I skipped this once and had 2,000 failed login attempts in the logs within an hour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor with &lt;code&gt;pm2 monit&lt;/code&gt;&lt;/strong&gt; before assuming things are fine — one of my workers had a memory leak that only showed up under load&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Full step-by-step guide with exact commands at &lt;a href="https://vpsfor.dev/posts/hetzner-cx22-setup-guide-2026/" rel="noopener noreferrer"&gt;vpsfor.dev →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How many Node.js apps can you run on a 2-core 1GB VPS with PM2? (The math)</title>
      <dc:creator>Vpsfordev</dc:creator>
      <pubDate>Wed, 03 Jun 2026 16:06:42 +0000</pubDate>
      <link>https://dev.to/vpsfordev/how-many-nodejs-apps-can-you-run-on-a-2-core-1gb-vps-with-pm2-the-math-ec4</link>
      <guid>https://dev.to/vpsfordev/how-many-nodejs-apps-can-you-run-on-a-2-core-1gb-vps-with-pm2-the-math-ec4</guid>
      <description>&lt;p&gt;Every indie hacker running on a $5 VPS has asked this question. The answer depends entirely on what kind of apps you're running — and most guides skip the actual math.&lt;/p&gt;

&lt;p&gt;Here's the breakdown I wish I had when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your real available RAM is not 1GB
&lt;/h2&gt;

&lt;p&gt;The OS and PM2 daemon eat RAM before your app runs a single line of code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;RAM Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu 22.04 (minimal)&lt;/td&gt;
&lt;td&gt;~180MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PM2 daemon&lt;/td&gt;
&lt;td&gt;~30MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Available for your apps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~790MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So you're working with ~790MB, not 1024MB. Now let's see what fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  App capacity by type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App Type&lt;/th&gt;
&lt;th&gt;Idle RAM&lt;/th&gt;
&lt;th&gt;Under Load&lt;/th&gt;
&lt;th&gt;Max Apps on 2-core 1GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple bot / webhook&lt;/td&gt;
&lt;td&gt;60–90MB&lt;/td&gt;
&lt;td&gt;~120MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6–8 apps&lt;/strong&gt; (fork mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard REST API (Express/Fastify)&lt;/td&gt;
&lt;td&gt;80–120MB&lt;/td&gt;
&lt;td&gt;~200MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2–3 apps&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js / Nuxt SSR&lt;/td&gt;
&lt;td&gt;350–500MB&lt;/td&gt;
&lt;td&gt;600MB+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 app only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy AI / Discord bot&lt;/td&gt;
&lt;td&gt;600MB+&lt;/td&gt;
&lt;td&gt;OOM risk&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 app + swap&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "under load" column is what actually matters — idle numbers are misleading because Node.js apps spike heavily during deploys, GC cycles, and traffic bursts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: One API, full cluster mode (2 workers)
&lt;/h2&gt;

&lt;p&gt;If you have a single Node.js API and want to use both CPU cores:&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;// ecosystem.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// exactly 2 workers for a 2-core VPS&lt;/span&gt;
    &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;350M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;node_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--max-old-space-size=320&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env_production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;2 workers × 350MB limit = 700MB max, leaving ~90MB headroom. The &lt;code&gt;--max-old-space-size&lt;/code&gt; flag is critical — without it, V8 can claim up to 1.5GB and OOM-kill your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: Multiple small apps, fork mode
&lt;/h2&gt;

&lt;p&gt;If you're hosting 2–3 separate projects on the same VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api/dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fork&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;250M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;node_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--max-old-space-size=200&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker/dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fork&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;node_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--max-old-space-size=160&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="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;Two apps × ~200MB = 400MB used, ~390MB free for spikes. Fork mode is better here than cluster — each app runs independently, and a crash in one doesn't affect the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 things I learned the hard way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always create a swap file — it's not optional on 1GB&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Deploys temporarily spike RAM usage by 30–50%. Without swap, a &lt;code&gt;npm install&lt;/code&gt; on a busy server will OOM-kill your running app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;max_memory_restart&lt;/code&gt; is a safety net, not a target&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If PM2 is regularly hitting the limit and restarting your app, that's a memory leak — fix the code, don't raise the limit. Use &lt;code&gt;pm2 monit&lt;/code&gt; to watch RSS (resident set size) over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Node.js v20+ saves you ~20% RAM for free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern V8 engines use pointer compression by default on 64-bit systems, reducing heap object size significantly. If you're still on Node 16/18, upgrading alone might give you room for one more small app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick decision guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 production API&lt;/strong&gt; → cluster mode, 2 instances, &lt;code&gt;max_memory_restart: '350M'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 small projects&lt;/strong&gt; → fork mode, 1 instance each, &lt;code&gt;max_memory_restart: '250M'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3+ bots/webhooks&lt;/strong&gt; → fork mode, &lt;code&gt;max_memory_restart: '120M'&lt;/code&gt;, add swap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js SSR&lt;/strong&gt; → 1 app only, upgrade to 2GB RAM if possible&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Full PM2 ecosystem configs for 1GB, 2GB, and 4GB VPS tiers at &lt;a href="https://vpsfor.dev/posts/pm2-vps-optimization-guide-2026/" rel="noopener noreferrer"&gt;vpsfor.dev →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I benchmarked Hetzner CX22 vs DigitalOcean for Node.js in 2026 — 5 cheaper, 4% slower</title>
      <dc:creator>Vpsfordev</dc:creator>
      <pubDate>Mon, 01 Jun 2026 16:27:02 +0000</pubDate>
      <link>https://dev.to/vpsfordev/i-benchmarked-hetzner-cx22-vs-digitalocean-for-nodejs-in-2026-5x-cheaper-4-slower-1j82</link>
      <guid>https://dev.to/vpsfordev/i-benchmarked-hetzner-cx22-vs-digitalocean-for-nodejs-in-2026-5x-cheaper-4-slower-1j82</guid>
      <description>&lt;p&gt;Most Node.js developers default to DigitalOcean because it's what every tutorial uses. I did too — until I checked the bill.&lt;/p&gt;

&lt;p&gt;A 4GB DigitalOcean Premium droplet costs &lt;strong&gt;$24/month&lt;/strong&gt;. A Hetzner CX22 with the same specs costs &lt;strong&gt;€4.35/month&lt;/strong&gt;. That's a 5× price difference. So I ran the benchmarks to find out what you actually give up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; &lt;code&gt;autocannon&lt;/code&gt; — 10 concurrent connections, 60-second sustained load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; Standard Express.js API returning JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; Node.js v25 on Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiers compared:&lt;/strong&gt; Both at 2 vCPU / 4GB RAM (equivalent specs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Throughput (requests per second)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;RPS&lt;/th&gt;
&lt;th&gt;Price/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner&lt;/td&gt;
&lt;td&gt;CX22 (2 vCPU, 4GB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,250&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~€4.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;Premium AMD (2 vCPU, 4GB)&lt;/td&gt;
&lt;td&gt;4,100&lt;/td&gt;
&lt;td&gt;~$24.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;Basic (1 vCPU, 2GB)&lt;/td&gt;
&lt;td&gt;2,300&lt;/td&gt;
&lt;td&gt;$6.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hetzner is &lt;strong&gt;~3.5% faster&lt;/strong&gt; than DigitalOcean Premium at the same spec tier. For a 5× price difference, that's not a typo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disk I/O — npm install speed
&lt;/h3&gt;

&lt;p&gt;Installing a real project with 300+ dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner CX22:&lt;/strong&gt; 18.4 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean Premium:&lt;/strong&gt; 21.2 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hetzner uses NVMe over Fabrics (NVMe-oF) which consistently outperformed DO in sequential read/write tests with &lt;code&gt;fio&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bandwidth included
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Included bandwidth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner CX22&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 TB/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean Premium&lt;/td&gt;
&lt;td&gt;4 TB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're serving large assets or have high-traffic APIs, this matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the price gap is this large
&lt;/h2&gt;

&lt;p&gt;Hetzner is a European DC operator with 30+ years in infrastructure. They don't have a venture-backed marketing budget or a $200M/year developer relations program. What they do have is AMD EPYC Milan/Genoa nodes, NVMe storage, and 20TB of transfer — at a price point that makes no sense until you use it.&lt;/p&gt;

&lt;p&gt;DigitalOcean Premium uses similar EPYC hardware, but you're paying for the platform: managed databases, one-click apps, a polished dashboard, live chat support, and 14+ global regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to pick which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hetzner CX22&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're comfortable with SSH, PM2, and Nginx&lt;/li&gt;
&lt;li&gt;Your users are in Europe or North America&lt;/li&gt;
&lt;li&gt;You want maximum Node.js throughput per euro&lt;/li&gt;
&lt;li&gt;You're a solo dev or small team that can handle their own incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need managed Postgres, Redis, or S3-compatible storage&lt;/li&gt;
&lt;li&gt;Your users are globally distributed (DO has 14+ regions vs Hetzner's 5)&lt;/li&gt;
&lt;li&gt;You need live chat support or hourly billing for ephemeral environments&lt;/li&gt;
&lt;li&gt;You're working with a team that can't deal with Linux emergencies at 2am&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;For 90% of solo developers and early-stage startups, Hetzner CX22 is the better choice. You get equivalent raw performance for a fraction of the cost. The only real trade-offs are fewer regions and ticket-only support.&lt;/p&gt;

&lt;p&gt;I migrated a Node.js API from DigitalOcean Basic ($12/mo) to Hetzner CX22 (€4.35/mo) and saw slightly better response times with the same PM2 + Nginx setup. The monthly bill dropped by ~$8 for a server that's technically more capable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full methodology, raw &lt;code&gt;fio&lt;/code&gt; and &lt;code&gt;autocannon&lt;/code&gt; output, and a complete setup guide at &lt;a href="https://vpsfor.dev/posts/hetzner-vs-digitalocean-nodejs-benchmarks-2026/" rel="noopener noreferrer"&gt;vpsfor.dev →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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