<?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: Vishvajeet Shukla</title>
    <description>The latest articles on DEV Community by Vishvajeet Shukla (@vishvajeetshukla).</description>
    <link>https://dev.to/vishvajeetshukla</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195744%2F674d09f0-2780-4966-a3f6-836122c4157a.gif</url>
      <title>DEV Community: Vishvajeet Shukla</title>
      <link>https://dev.to/vishvajeetshukla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishvajeetshukla"/>
    <language>en</language>
    <item>
      <title>PM2 vs systemd for Node.js in Production: Which Should You Use? (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:14:48 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/pm2-vs-systemd-for-nodejs-in-production-which-should-you-use-2026-4ngc</link>
      <guid>https://dev.to/vishvajeetshukla/pm2-vs-systemd-for-nodejs-in-production-which-should-you-use-2026-4ngc</guid>
      <description>&lt;p&gt;Every Node.js app on a VPS needs something to start it, restart it when it crashes and bring it back after a reboot. The two common answers are PM2 and systemd. Both work. They're good at different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PM2&lt;/strong&gt; if you want cluster mode, &lt;code&gt;pm2 reload&lt;/code&gt; with no dropped requests, and friendly per-app commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;systemd&lt;/strong&gt; if you want zero extra dependencies, strict sandboxing and the same tool you use for every other service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What each one is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;systemd&lt;/strong&gt; is the init system on almost every modern Linux distro. It already runs Nginx, SSH and cron. A Node app becomes one more unit file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PM2&lt;/strong&gt; is a Node.js process manager installed via npm. It runs its own daemon, which manages your apps and is itself started by systemd through &lt;code&gt;pm2 startup&lt;/code&gt;. ShipOwn's guide &lt;a href="https://shipown.com/blog/what-is-pm2" rel="noopener noreferrer"&gt;What Is PM2?&lt;/a&gt; covers it in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PM2&lt;/th&gt;
&lt;th&gt;systemd&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm i -g pm2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-restart on crash&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Restart=always&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start on boot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm2 startup&lt;/code&gt; + &lt;code&gt;pm2 save&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl enable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster mode across CPU cores&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;td&gt;You run multiple units or use Node's cluster module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-downtime reload&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pm2 reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not out of the box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.pm2/logs&lt;/code&gt;, &lt;code&gt;pm2 logs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;journald, &lt;code&gt;journalctl -u&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log rotation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm2-logrotate&lt;/code&gt; module&lt;/td&gt;
&lt;td&gt;journald handles it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory limit restart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_memory_restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MemoryMax=&lt;/code&gt; (kills), plus &lt;code&gt;Restart=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandboxing&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Strong (&lt;code&gt;ProtectSystem&lt;/code&gt;, &lt;code&gt;NoNewPrivileges&lt;/code&gt;...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring view&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pm2 monit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl status&lt;/code&gt;, external tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A minimal systemd unit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;My Node API&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/api/current&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/api/shared/.env&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/node dist/main.js&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Install]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&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 &lt;/span&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ShipOwn's free &lt;a href="https://shipown.com/tools/systemd-service-generator" rel="noopener noreferrer"&gt;systemd service file generator&lt;/a&gt; builds one of these in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal PM2 setup
&lt;/h2&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;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/main.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;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/srv/api/current&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;'&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;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;400M&lt;/span&gt;&lt;span class="dl"&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;pm2 start ecosystem.config.js &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pm2 save &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When PM2 is the better choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You run several Node apps on one server and want one consistent set of commands.&lt;/li&gt;
&lt;li&gt;You want to use all CPU cores without writing cluster code.&lt;/li&gt;
&lt;li&gt;You deploy often and want &lt;code&gt;pm2 reload&lt;/code&gt; to roll workers without dropping requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When systemd is the better choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You run a single service and don't want another daemon or global npm package.&lt;/li&gt;
&lt;li&gt;Security hardening matters and you want systemd's sandbox options.&lt;/li&gt;
&lt;li&gt;Your team already manages everything with &lt;code&gt;systemctl&lt;/code&gt; and journald.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistakes with either
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running as root.&lt;/strong&gt; Use a deploy user in both cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting reboot persistence.&lt;/strong&gt; &lt;code&gt;pm2 save&lt;/code&gt; after changes; &lt;code&gt;systemctl enable&lt;/code&gt; for units. Test with a real reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded logs.&lt;/strong&gt; Install &lt;code&gt;pm2-logrotate&lt;/code&gt;, or check journald's &lt;code&gt;SystemMaxUse&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Env changes not applied.&lt;/strong&gt; &lt;code&gt;pm2 reload --update-env&lt;/code&gt;, or &lt;code&gt;systemctl restart&lt;/code&gt; after editing the &lt;code&gt;EnvironmentFile&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My choice
&lt;/h2&gt;

&lt;p&gt;For Node apps I deploy often, I use PM2, mainly for reloads and cluster mode. It's also what &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;, the desktop app I built for deploying to servers you already own, uses for Node apps: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify and Express run under PM2, and you can see PM2 processes next to CPU, RAM, disk and a live terminal in the &lt;a href="https://shipown.com/features/health-terminal" rel="noopener noreferrer"&gt;health view&lt;/a&gt;. For a single small internal service, a systemd unit is hard to beat.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://www.vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and founder of &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>pm2</category>
      <category>systemd</category>
      <category>linux</category>
    </item>
    <item>
      <title>502 Bad Gateway After a Deploy? How to Debug and Roll Back on a VPS (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:40:08 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/502-bad-gateway-after-a-deploy-how-to-debug-and-roll-back-on-a-vps-2026-4mih</link>
      <guid>https://dev.to/vishvajeetshukla/502-bad-gateway-after-a-deploy-how-to-debug-and-roll-back-on-a-vps-2026-4mih</guid>
      <description>&lt;p&gt;You deploy, refresh the page and see &lt;strong&gt;502 Bad Gateway&lt;/strong&gt;. Users are hitting it too. The worst thing you can do now is start changing things at random over SSH.&lt;/p&gt;

&lt;p&gt;This post is a playbook: first get the site back (rollback), then find the cause (debugging), then prevent it next time. Along the way I'll show how &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; handles each step on servers you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a 502 actually means
&lt;/h2&gt;

&lt;p&gt;Nginx is up and received the request, but when it forwarded the request to your app (the "upstream"), it got no valid response. So the problem is almost always &lt;strong&gt;between Nginx and the app&lt;/strong&gt;, not DNS or HTTPS.&lt;/p&gt;

&lt;p&gt;Typical causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app process crashed or never started.&lt;/li&gt;
&lt;li&gt;The app listens on a different port than Nginx expects.&lt;/li&gt;
&lt;li&gt;The app is still starting (slow boot) or stuck.&lt;/li&gt;
&lt;li&gt;The server ran out of memory or disk.&lt;/li&gt;
&lt;li&gt;A Docker container is bound to a different interface or port.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 0: Restore service first
&lt;/h2&gt;

&lt;p&gt;If the site worked before this deploy, &lt;strong&gt;roll back before you debug&lt;/strong&gt;. Users don't care why; they care that it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual rollback
&lt;/h3&gt;

&lt;p&gt;If you deploy into release folders with a &lt;code&gt;current&lt;/code&gt; symlink:&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;cd&lt;/span&gt; /var/www/app
&lt;span class="nb"&gt;ls &lt;/span&gt;releases/                       &lt;span class="c"&gt;# find the previous release&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; releases/20261027-1830 current
pm2 reload app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you deploy in place with &lt;code&gt;git pull&lt;/code&gt;, rollback means checking out the previous commit and rebuilding, which is slow and stressful mid-incident. That alone is a good reason to switch to release folders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback with ShipOwn
&lt;/h3&gt;

&lt;p&gt;ShipOwn puts &lt;strong&gt;every deploy in its own release directory&lt;/strong&gt; and only moves the live pointer after the build and health check pass. If a deploy fails those checks, &lt;strong&gt;the previous release is restored automatically&lt;/strong&gt;. If the problem appears later, you can switch back to a retained release from the app's operate page (or to an earlier image for Docker apps), while its files are still on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Is the app running?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 status
pm2 logs app &lt;span class="nt"&gt;--lines&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;errored&lt;/code&gt;, a climbing restart count, or a stack trace. Common culprits after a deploy: a missing environment variable, a failed build that left no output, or a dependency that didn't install.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
docker logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &amp;lt;container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Is it on the right port?
&lt;/h2&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;ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'3000|8000'&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; proxy_pass /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port in &lt;code&gt;proxy_pass&lt;/code&gt; must match what the app actually listens on. A changed &lt;code&gt;PORT&lt;/code&gt; variable is a classic cause.&lt;/p&gt;

&lt;p&gt;Test the app directly, bypassing Nginx:&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;-I&lt;/span&gt; http://127.0.0.1:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that works, the problem is Nginx config. If it fails, it's the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check the Nginx error log
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50 /var/log/nginx/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connect() failed (111: Connection refused)&lt;/code&gt; means nothing is listening. &lt;code&gt;upstream timed out&lt;/code&gt; means the app is too slow or stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Check resources
&lt;/h2&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;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;uptime
&lt;/span&gt;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'killed process|oom'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full disk breaks builds and logs. An out-of-memory kill silently stops your app. Both are common on small VPS plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fix, then redeploy
&lt;/h2&gt;

&lt;p&gt;Once you know the cause, fix it in the code or config and deploy again. Not by editing files on the live server, or your next deploy will overwrite the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging with an approval-based AI terminal
&lt;/h2&gt;

&lt;p&gt;Mid-incident, remembering the right command is the hard part. ShipOwn includes an &lt;strong&gt;AI terminal&lt;/strong&gt; for exactly this moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You describe the problem in plain language: "the site returns 502 since the last deploy".&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;your own&lt;/strong&gt; Anthropic, OpenAI or DeepSeek API key, the model inspects the server and &lt;strong&gt;proposes one shell command at a time, with its reason&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You approve each command&lt;/strong&gt; before it runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive commands&lt;/strong&gt; (recursive deletes, reboots, firewall flushes, stopping SSH) are &lt;strong&gt;always held for confirmation&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI never runs on ShipOwn's servers. Requests go from your computer to the provider you chose, and your key is encrypted by the OS. Prompts and command output do go to that provider, so check output for secrets before sharing.&lt;/p&gt;

&lt;p&gt;Next to it, ShipOwn's server view shows CPU, RAM, disk, load average and PM2 processes, read directly from the server, plus an embedded terminal and logs. You can usually spot the out-of-memory or full-disk case at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent the next 502
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release directories + health checks.&lt;/strong&gt; Don't switch traffic until the new version answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Nginx before reload.&lt;/strong&gt; ShipOwn tests every config it writes and restores it if Nginx rejects it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build where there's memory.&lt;/strong&gt; ShipOwn's PC build mode builds on your machine and uploads the verified output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set restart policies.&lt;/strong&gt; PM2 or &lt;code&gt;restart: unless-stopped&lt;/code&gt; for containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get told when it's down.&lt;/strong&gt; ShipOwn's tray heartbeats report an "unreachable" incident after two consecutive failures, and report recovery once (while the app is running and your PC is awake).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know what's live.&lt;/strong&gt; ShipOwn shows the commit each app runs and how far behind its branch it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest limit: no deploy tool rolls back your &lt;strong&gt;database&lt;/strong&gt;. If a release ran a migration, you need a separate plan for migrations and backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;502 right after deploy&lt;/td&gt;
&lt;td&gt;Roll back, then &lt;code&gt;pm2 logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Connection refused&lt;/code&gt; in Nginx log&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ss -ltnp&lt;/code&gt; to check the port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upstream timed out&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App logs, &lt;code&gt;uptime&lt;/code&gt;, slow queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App randomly dies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dmesg&lt;/code&gt; for OOM kills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build fails&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;df -h&lt;/code&gt;, &lt;code&gt;free -h&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;A 502 is stressful but predictable. Roll back first, then check the process, the port, the Nginx log and the server's resources. If you want release-based rollback and approval-gated AI debugging on your own VPS, &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;try ShipOwn free for 30 days&lt;/a&gt;, no card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>debugging</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>ShipOwn vs Coolify vs CapRover vs Dokploy: Which Self-Hosted Deploy Tool Fits You? (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:40:01 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/shipown-vs-coolify-vs-caprover-vs-dokploy-which-self-hosted-deploy-tool-fits-you-2026-378l</link>
      <guid>https://dev.to/vishvajeetshukla/shipown-vs-coolify-vs-caprover-vs-dokploy-which-self-hosted-deploy-tool-fits-you-2026-378l</guid>
      <description>&lt;p&gt;If you want Heroku- or Vercel-style deploys on your own server, four names come up again and again: &lt;strong&gt;Coolify&lt;/strong&gt;, &lt;strong&gt;CapRover&lt;/strong&gt;, &lt;strong&gt;Dokploy&lt;/strong&gt; and &lt;strong&gt;ShipOwn&lt;/strong&gt;. They solve the same problem, getting your app from a repo to a live URL on hardware you control, but they make very different design choices.&lt;/p&gt;

&lt;p&gt;Full disclosure: I built &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;. I'll stick to architecture and documented behaviour, and say plainly where the other tools are the better pick. Feature lists change fast, so check each project's docs before deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one difference that matters most
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coolify, CapRover and Dokploy are platforms you install on the server.&lt;/strong&gt; A web dashboard and its supporting services run on your VPS, and your apps are deployed as Docker containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ShipOwn is a desktop app.&lt;/strong&gt; It runs on your Windows, macOS or Linux computer and connects to the server over plain SSH. No ShipOwn deployment agent is installed on the server. The server stays a normal Linux box running PM2, Nginx and, only if you want it, Docker.&lt;/p&gt;

&lt;p&gt;Almost every other difference follows from this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Coolify&lt;/th&gt;
&lt;th&gt;CapRover&lt;/th&gt;
&lt;th&gt;Dokploy&lt;/th&gt;
&lt;th&gt;ShipOwn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;On your server&lt;/td&gt;
&lt;td&gt;On your server&lt;/td&gt;
&lt;td&gt;On your server&lt;/td&gt;
&lt;td&gt;On your computer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;Web dashboard&lt;/td&gt;
&lt;td&gt;Web dashboard + CLI&lt;/td&gt;
&lt;td&gt;Web dashboard&lt;/td&gt;
&lt;td&gt;Desktop app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps run as&lt;/td&gt;
&lt;td&gt;Docker containers&lt;/td&gt;
&lt;td&gt;Docker containers (Swarm)&lt;/td&gt;
&lt;td&gt;Docker containers&lt;/td&gt;
&lt;td&gt;PM2 (Node), Nginx (static) or Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker required&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source code&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;Commercial, closed source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-deploy on push&lt;/td&gt;
&lt;td&gt;Yes (webhooks)&lt;/td&gt;
&lt;td&gt;Yes (webhooks)&lt;/td&gt;
&lt;td&gt;Yes (webhooks)&lt;/td&gt;
&lt;td&gt;No, manual deploy with "commits waiting" indicator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team access via browser&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, per-device desktop licence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Coolify
&lt;/h2&gt;

&lt;p&gt;Coolify is a popular open-source, self-hostable PaaS. Install it on a server and you get a web dashboard to deploy apps from Git, run databases and services, and manage multiple servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Coolify if:&lt;/strong&gt; you want a browser dashboard your whole team can use, you're happy with everything in Docker, and you like the open-source model and community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider:&lt;/strong&gt; the platform itself uses CPU, memory and disk on your server, and it's another piece of software on the server you need to keep updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  CapRover
&lt;/h2&gt;

&lt;p&gt;CapRover is one of the older open-source options. It runs on Docker Swarm, offers a web UI and CLI, and has a catalogue of one-click apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose CapRover if:&lt;/strong&gt; you want something battle-tested, simple &lt;code&gt;captain-definition&lt;/code&gt; deploys, or a multi-node Swarm cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider:&lt;/strong&gt; everything is containerised, and Swarm adds concepts you may not need on a single small VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dokploy
&lt;/h2&gt;

&lt;p&gt;Dokploy is a newer open-source platform with a modern UI, strong Docker and Docker Compose support, and database management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Dokploy if:&lt;/strong&gt; you live in Compose files and want a clean, modern dashboard on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider:&lt;/strong&gt; like Coolify, it's a control plane running next to your apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  ShipOwn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; brings the platform workflow to your desktop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect a server&lt;/strong&gt; over SSH (tested before it's saved).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a source&lt;/strong&gt;: a GitHub repo and branch, or a local folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the plan&lt;/strong&gt;: framework, commands, port, domain and target, all editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it build&lt;/strong&gt; on the server, or on your PC with the verified output uploaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate it&lt;/strong&gt;: live URL, deployed commit, health, logs, redeploy, rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It deploys three kinds of apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node apps under PM2&lt;/strong&gt;: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify, Express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static sites on Nginx&lt;/strong&gt;: Vite (React/Vue), Astro, plain HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker apps&lt;/strong&gt; from a Dockerfile or Compose file, with commit-labelled images and the previous container kept until the health check passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other documented features: Nginx virtual hosts tested before reload and restored if rejected, Certbot HTTPS with expiry shown, a release directory per deploy with automatic restore on failure, server vitals read from &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;pm2 jlist&lt;/code&gt;, an embedded terminal, file browser, fail2ban setup, management of existing containers, and an AI terminal that uses your own Anthropic, OpenAI or DeepSeek key and proposes one command at a time for your approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose ShipOwn if:&lt;/strong&gt; you want nothing extra running on the server, you deploy Node apps without Docker, you build on your own machine to spare a small VPS, or you want approval-gated AI help for server problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider:&lt;/strong&gt; there's no auto-deploy on push, no browser dashboard for a team, and it's a commercial product, not open source. It uses a hosted licence service and update service, so it isn't fully offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"My team needs a shared web dashboard."&lt;/strong&gt; → Coolify or Dokploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I want everything containerised, maybe across nodes."&lt;/strong&gt; → CapRover or Dokploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I need auto-deploy on every push."&lt;/strong&gt; → Coolify, CapRover or Dokploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I want my server to run only my apps, no platform."&lt;/strong&gt; → ShipOwn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"My Next.js app runs fine on PM2; I don't want Docker."&lt;/strong&gt; → ShipOwn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I want open source."&lt;/strong&gt; → Coolify, CapRover or Dokploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Can you combine them?
&lt;/h2&gt;

&lt;p&gt;Partly. ShipOwn can inspect and manage Docker containers that are already on a server, but running two deployment systems against the same apps will cause confusion. Pick one owner per app.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which one is the "best"?&lt;/strong&gt; None in general. The architecture question (dashboard on the server vs app on your desktop) usually decides it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which is lightest on the server?&lt;/strong&gt; ShipOwn adds no platform process to the server; the others run their dashboard and services there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I try ShipOwn without paying?&lt;/strong&gt; Yes, a free 30-day trial with no card. Check the &lt;a href="https://shipown.com/docs/supported-environments" rel="noopener noreferrer"&gt;supported environments&lt;/a&gt; first.&lt;/p&gt;

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

&lt;p&gt;Coolify, CapRover and Dokploy are excellent open-source platforms if you want a dashboard and containers on the server. ShipOwn is for developers who'd rather keep the server plain and drive deploys from their own machine. Try the one whose architecture fits how you work, and if that's the desktop approach, start with &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Cut Hosting Costs by Self-Hosting Your Apps on a VPS (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:37:32 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/how-to-cut-hosting-costs-by-self-hosting-your-apps-on-a-vps-2026-4joe</link>
      <guid>https://dev.to/vishvajeetshukla/how-to-cut-hosting-costs-by-self-hosting-your-apps-on-a-vps-2026-4joe</guid>
      <description>&lt;p&gt;Managed platforms are priced per app, per seat, per build minute or per gigabyte of bandwidth. That's fair when you have one big app. It adds up fast when you're a freelancer or a small team running ten small ones: client sites, internal tools, side projects, staging copies.&lt;/p&gt;

&lt;p&gt;A single Linux VPS can often host all of them. This guide explains how to work out whether self-hosting actually saves &lt;em&gt;you&lt;/em&gt; money, how to consolidate apps safely, and where tools like &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; reduce the time cost that usually eats the savings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We won't quote prices here because they change constantly and depend on your region and usage. Use your own invoices for the maths below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Audit what you pay for today
&lt;/h2&gt;

&lt;p&gt;Make a simple table from your last three invoices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;Traffic&lt;/th&gt;
&lt;th&gt;Needs always-on?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client site A&lt;/td&gt;
&lt;td&gt;PaaS&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal tool&lt;/td&gt;
&lt;td&gt;PaaS&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;very low&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;PaaS + DB add-on&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-app minimums&lt;/strong&gt; on apps that get almost no traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add-ons&lt;/strong&gt; (databases, Redis, logging) priced separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat-based pricing&lt;/strong&gt; for people who only deploy occasionally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bandwidth or function-invocation overages.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Size one server for the whole group
&lt;/h2&gt;

&lt;p&gt;Low-traffic Node apps under PM2 and static sites served by Nginx use little memory when idle. A static site uses no process at all, since Nginx serves files directly. That's why consolidation works.&lt;/p&gt;

&lt;p&gt;Rough method:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check each app's memory in production (&lt;code&gt;pm2 status&lt;/code&gt; or the platform's metrics).&lt;/li&gt;
&lt;li&gt;Add them up, plus headroom for builds (builds are the memory spike, not runtime).&lt;/li&gt;
&lt;li&gt;Add the database if you plan to host it on the same server (many people keep a managed DB instead).&lt;/li&gt;
&lt;li&gt;Pick a plan with comfortable margin. Resizing a VPS later is usually easy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; if builds are the bottleneck, build on your own machine and upload the output. ShipOwn's PC build mode does exactly this, so a small server only has to &lt;em&gt;run&lt;/em&gt; the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Count the hidden cost: your time
&lt;/h2&gt;

&lt;p&gt;Self-hosting isn't free. You now own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OS updates and security patches&lt;/li&gt;
&lt;li&gt;Nginx configs and HTTPS renewals&lt;/li&gt;
&lt;li&gt;Process restarts and crash recovery&lt;/li&gt;
&lt;li&gt;Rollback when a deploy breaks&lt;/li&gt;
&lt;li&gt;Monitoring and "is it down?" checks&lt;/li&gt;
&lt;li&gt;Backups, especially for databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those take you several hours a month, compare that time to what you'd save. This is the line that decides whether self-hosting is cheaper, and it's where automation matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Consolidate safely
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Give every app its own domain and port
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client-a.example.com  → Nginx → 127.0.0.1:3001 (PM2)
tool.example.com      → Nginx → 127.0.0.1:3002 (PM2)
www.example.com       → Nginx → /var/www/site (static)
api.example.com       → Nginx → 127.0.0.1:8000 (Docker)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Isolate failures
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One bad Nginx config can break every site. &lt;strong&gt;Test before every reload.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;One app leaking memory can starve the others. Set PM2 &lt;code&gt;max_memory_restart&lt;/code&gt; or container memory limits.&lt;/li&gt;
&lt;li&gt;Keep releases separate so rolling back app A doesn't touch app B.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Watch the shared resources
&lt;/h3&gt;

&lt;p&gt;Disk fills up from logs, old builds and Docker images. Check CPU, RAM, disk and load regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Migrate one app at a time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the app to the VPS on a temporary subdomain.&lt;/li&gt;
&lt;li&gt;Test it, including env vars and webhooks.&lt;/li&gt;
&lt;li&gt;Lower the DNS TTL, then switch the real domain.&lt;/li&gt;
&lt;li&gt;Keep the old platform running for a few days.&lt;/li&gt;
&lt;li&gt;Cancel the old plan only when you're confident.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How ShipOwn lowers the time cost
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; is a desktop app for Windows, macOS and Linux that deploys and operates apps on Linux servers you own, over plain SSH, without installing a deployment agent on the server. It targets the maintenance tasks listed in Step 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploys&lt;/strong&gt; Node apps under PM2 (Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify, Express), static sites on Nginx (Vite, Astro, plain HTML) and Docker/Compose apps, through one five-step flow: connect, pick source, review plan, watch build, operate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx and HTTPS&lt;/strong&gt;: writes virtual hosts, tests before reload, restores a rejected config, requests Certbot certificates and shows expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback&lt;/strong&gt;: each deploy has its own release directory, a failed deploy restores the previous release automatically, and you can switch back to a retained release later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: CPU, RAM, disk, load and PM2 processes read directly from the server, plus tray heartbeats that report an "unreachable" incident after two consecutive failures (while the app runs and your PC is awake).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security basics&lt;/strong&gt;: fail2ban can be installed and viewed from the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt;: an AI terminal using your own Anthropic, OpenAI or DeepSeek key proposes one command at a time for your approval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also shows which commit each app runs and how far behind its branch it is, which is handy when you have many small apps on one box.&lt;/p&gt;

&lt;p&gt;What it doesn't do: auto-deploy on push, and it doesn't replace a backup plan for your databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  When self-hosting is &lt;em&gt;not&lt;/em&gt; cheaper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You have one app that fits a free or low tier already.&lt;/li&gt;
&lt;li&gt;You rely heavily on platform features like preview deployments or a global edge network.&lt;/li&gt;
&lt;li&gt;Nobody on the team is willing to own the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I keep my database managed and self-host only the apps?&lt;/strong&gt; Yes, and for many teams that's the best balance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many apps can one VPS run?&lt;/strong&gt; It depends on memory and traffic. Measure, then size with headroom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is ShipOwn free?&lt;/strong&gt; It starts with a free 30-day trial, no card required, then paid plans.&lt;/p&gt;

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

&lt;p&gt;Self-hosting saves money when you consolidate several small apps and keep the maintenance time low. Measure your real costs, size one server sensibly, migrate one app at a time, and let &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; handle the repetitive deploy, HTTPS and rollback work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>devops</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Custom Domain + Free SSL on a VPS: Nginx and Certbot Done Right (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:37:19 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/custom-domain-free-ssl-on-a-vps-nginx-and-certbot-done-right-2026-4llf</link>
      <guid>https://dev.to/vishvajeetshukla/custom-domain-free-ssl-on-a-vps-nginx-and-certbot-done-right-2026-4llf</guid>
      <description>&lt;p&gt;Your app is running on the server. &lt;code&gt;curl localhost:3000&lt;/code&gt; works. Now you want &lt;code&gt;https://app.example.com&lt;/code&gt; with a padlock, and that last mile (DNS, Nginx, certificates, renewals) is where a surprising number of deploys go wrong.&lt;/p&gt;

&lt;p&gt;This guide covers custom domains and free HTTPS on a Linux VPS end to end, plus how &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; automates it safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pieces fit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt; maps &lt;code&gt;app.example.com&lt;/code&gt; to your server's IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; listens on ports 80/443 and decides which app gets each request, based on the &lt;code&gt;server_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certbot&lt;/strong&gt; proves to Let's Encrypt that you control the domain and installs a certificate into Nginx.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewal&lt;/strong&gt; replaces the certificate before it expires.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: DNS records
&lt;/h2&gt;

&lt;p&gt;At your domain registrar or DNS provider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your server's IPv4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAAA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your server's IPv6 (only if Nginx listens on IPv6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;server IP (for the root domain, if needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check propagation before going further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it doesn't return your IP yet, Certbot will fail. Wait.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you use Cloudflare's proxy (orange cloud), set it to DNS-only while you issue the certificate, or use Cloudflare's own certificates with "Full (strict)".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Open the firewall
&lt;/h2&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;ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check your cloud provider's firewall or security group: ports 80 and 443 must be open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: An Nginx server block
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/sites-available/app.example.com&lt;/span&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;listen&lt;/span&gt; &lt;span class="s"&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;app.example.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://127.0.0.1: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;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_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;For a static site, use &lt;code&gt;root /var/www/site;&lt;/code&gt; and &lt;code&gt;try_files&lt;/code&gt; instead of &lt;code&gt;proxy_pass&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test before you reload. Always.
&lt;/h2&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/app.example.com /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="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx serves every site on the server from one process. If you reload a broken config, you can take down all of them. &lt;code&gt;nginx -t&lt;/code&gt; is the cheapest insurance in DevOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Issue the certificate
&lt;/h2&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; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-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; app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot answers the HTTP challenge, edits your server block to add &lt;code&gt;listen 443 ssl&lt;/code&gt;, certificate paths and (optionally) an HTTP→HTTPS redirect.&lt;/p&gt;

&lt;p&gt;Multiple names on one certificate:&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;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; example.com &lt;span class="nt"&gt;-d&lt;/span&gt; www.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Renewals
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt certificates are short-lived, so renewal must be automatic. The Certbot package usually installs a systemd timer. Verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-timers | &lt;span class="nb"&gt;grep &lt;/span&gt;certbot
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: www vs non-www and HTTP to HTTPS
&lt;/h2&gt;

&lt;p&gt;Pick one canonical host and redirect the other. Search engines treat &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt; as different URLs, so serving the same content on both splits your signals.&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;443&lt;/span&gt; &lt;span class="s"&gt;ssl&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;www.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;# ssl_certificate lines added by Certbot&lt;/span&gt;
  &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&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;example.com&lt;/span&gt; &lt;span class="s"&gt;www.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&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;Also check that your app generates absolute URLs (sitemaps, Open Graph tags, OAuth callbacks) with the final HTTPS host. Many "redirect loop" and "OAuth mismatch" bugs after a domain change come from an old &lt;code&gt;BASE_URL&lt;/code&gt; or &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; environment variable, not from Nginx.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick verification checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl -I http://app.example.com&lt;/code&gt; returns a 301 to HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl -I https://app.example.com&lt;/code&gt; returns 200 from your app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo certbot certificates&lt;/code&gt; lists the domain with a future expiry date.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo nginx -t&lt;/code&gt; is clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS not propagated yet&lt;/strong&gt; → challenge fails. Check with &lt;code&gt;dig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port 80 blocked&lt;/strong&gt; → the HTTP challenge can't reach you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two server blocks with the same &lt;code&gt;server_name&lt;/code&gt;&lt;/strong&gt; → Nginx warns and picks one; requests go to the wrong app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgot &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;&lt;/strong&gt; → the app thinks it's on HTTP and creates redirect loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewal silently failing&lt;/strong&gt; → you find out when browsers show a warning. Check expiry regularly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Domains and HTTPS with ShipOwn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; is a desktop app (Windows, macOS, Linux) that deploys apps to your Linux server over SSH, with no deployment agent installed on the server. Domains and HTTPS are built into the deploy flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you point a domain at an app or static site, ShipOwn &lt;strong&gt;writes the Nginx virtual host&lt;/strong&gt; for it.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;tests the configuration before reloading&lt;/strong&gt; Nginx.&lt;/li&gt;
&lt;li&gt;If Nginx &lt;strong&gt;rejects the config, it's put back the way it was&lt;/strong&gt;, so one app's domain can't break the others.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;asks Certbot for a certificate&lt;/strong&gt; when you want HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate expiry is shown in the app&lt;/strong&gt;, so a failing renewal doesn't stay hidden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The domain is part of the deploy plan you review before anything runs, next to the framework, commands, port and target. Every value is editable.&lt;/p&gt;

&lt;p&gt;ShipOwn also helps when the server moves: &lt;strong&gt;if a server's IP changes, it traces the server through its own domains&lt;/strong&gt; and confirms it by its SSH host key.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need a paid SSL certificate?&lt;/strong&gt; No. Let's Encrypt certificates are free and trusted by all major browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I host several domains on one VPS?&lt;/strong&gt; Yes. Each gets its own server block; Nginx routes by &lt;code&gt;server_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ShipOwn change my existing Nginx sites?&lt;/strong&gt; It writes virtual hosts for the apps you deploy with it and tests every change before reloading.&lt;/p&gt;

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

&lt;p&gt;DNS, a clean server block, &lt;code&gt;nginx -t&lt;/code&gt;, Certbot and working renewals: that's all custom-domain HTTPS really is. Do it carefully once, and let &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; repeat it for every app with the safety checks built in. Free 30-day trial, no card needed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>ssl</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Docker Deploy Without a DevOps Engineer: Dockerfile to Live HTTPS in 6 Steps (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:34:52 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/docker-deploy-without-a-devops-engineer-dockerfile-to-live-https-in-6-steps-2026-lfk</link>
      <guid>https://dev.to/vishvajeetshukla/docker-deploy-without-a-devops-engineer-dockerfile-to-live-https-in-6-steps-2026-lfk</guid>
      <description>&lt;p&gt;"It works in Docker on my laptop" is a great start. Getting that container onto a real server, behind a domain, with HTTPS and a way back when an update breaks, is where many small teams stall. Usually because nobody on the team is "the DevOps person".&lt;/p&gt;

&lt;p&gt;You don't need one for a single server. This guide takes a Dockerfile or Compose project from your repo to a live HTTPS URL in six steps, explains the parts people get wrong, and then shows how &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; automates the same path from a desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Docker on a VPS at all?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any language.&lt;/strong&gt; Python, Go, Rust, PHP, Ruby, Java and .NET apps all run the same way once they're in a container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same environment everywhere.&lt;/strong&gt; The image you test is the image you run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean server.&lt;/strong&gt; Dependencies live in the image, not on the host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app is plain Node.js, you may not need Docker at all; PM2 is lighter. Docker earns its place when you have non-Node services or several services wired together with Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Docker properly
&lt;/h2&gt;

&lt;p&gt;On Ubuntu, install Docker Engine and the Compose plugin from Docker's official repository (or with the convenience script for a quick test):&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;-fsSL&lt;/span&gt; https://get.docker.com | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;   &lt;span class="c"&gt;# log out and in again&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Build on the server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/you/service.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;service
docker compose build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag images with the commit you built from, so you always know what's running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; service:&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Bind to localhost, not the world
&lt;/h2&gt;

&lt;p&gt;This is the most common Docker-on-VPS mistake. &lt;code&gt;ports: "8000:8000"&lt;/code&gt; publishes the port on every interface, and Docker's own firewall rules can bypass &lt;code&gt;ufw&lt;/code&gt;. Bind to loopback instead and let Nginx be the only public entry:&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;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;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:8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;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"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/health"&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;30s&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Start it and check health
&lt;/h2&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;
docker compose ps        &lt;span class="c"&gt;# look for "healthy"&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt; brings the container back after a crash or reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Nginx and HTTPS
&lt;/h2&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;server_name&lt;/span&gt; &lt;span class="s"&gt;service.example.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://127.0.0.1:8000&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-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="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 &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; service.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Update safely and keep a way back
&lt;/h2&gt;

&lt;p&gt;The risky update is &lt;code&gt;git pull &amp;amp;&amp;amp; docker compose up -d --build&lt;/code&gt; on a live service. If the new image starts but is broken, you're down until you fix it.&lt;/p&gt;

&lt;p&gt;A safer pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the new image with a new tag.&lt;/li&gt;
&lt;li&gt;Start it alongside the old one on a different localhost port.&lt;/li&gt;
&lt;li&gt;Wait for its health check to pass.&lt;/li&gt;
&lt;li&gt;Switch Nginx to the new port, &lt;code&gt;nginx -t&lt;/code&gt;, reload.&lt;/li&gt;
&lt;li&gt;Keep the old image for a quick rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works, but it's a lot of steps to repeat by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it with ShipOwn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; is a desktop app for Windows, macOS and Linux that deploys over plain SSH. No ShipOwn deployment agent is installed on the server. For Docker apps it follows the safe pattern above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds your &lt;strong&gt;Dockerfile or Compose file&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels images with the commit&lt;/strong&gt; they came from.&lt;/li&gt;
&lt;li&gt;Routes through &lt;strong&gt;Nginx to &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/strong&gt; when configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the previous container until the health check passes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Lets you go back to an &lt;strong&gt;earlier image&lt;/strong&gt; later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deploy flow is the same as for every ShipOwn app: connect a server, pick a GitHub branch or local folder, review an editable plan, watch the build stream live, then operate it from one page. If the server has no Docker yet, &lt;strong&gt;ShipOwn can install it&lt;/strong&gt; for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing containers you already have
&lt;/h3&gt;

&lt;p&gt;ShipOwn also works with containers already running on the server. It inspects every container, maps published and internal ports, shows CPU and memory gauges, calls out crash loops, and gives you start, stop and restart, a shell inside the container, and its logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domains and certificates
&lt;/h3&gt;

&lt;p&gt;Point a domain at the app and ShipOwn writes the Nginx virtual host, tests it before reloading, restores it if Nginx rejects it, and asks Certbot for a certificate. Certificate expiry is shown in the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container restarts in a loop&lt;/td&gt;
&lt;td&gt;App crashes on start; read &lt;code&gt;docker logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;502 from Nginx&lt;/td&gt;
&lt;td&gt;Wrong port, or container bound to a different interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service reachable on &lt;code&gt;IP:8000&lt;/code&gt; from the internet&lt;/td&gt;
&lt;td&gt;Port published on &lt;code&gt;0.0.0.0&lt;/code&gt; instead of &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk full&lt;/td&gt;
&lt;td&gt;Old images and build cache; &lt;code&gt;docker system prune&lt;/code&gt; with care&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Env vars missing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; not loaded or wrong &lt;code&gt;env_file&lt;/code&gt; path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stuck on something else? ShipOwn's AI terminal uses your own Anthropic, OpenAI or DeepSeek key to propose one command at a time, with its reason. You approve each one, and destructive commands always wait for confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does ShipOwn use Kubernetes or Swarm?&lt;/strong&gt; No. It builds and runs containers on a single Linux server you control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Docker required for every app?&lt;/strong&gt; No. Node apps run under PM2 and static sites are served by Nginx without Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is ShipOwn open source?&lt;/strong&gt; No, it's a commercial app. What runs on your server is plain SSH and shell commands.&lt;/p&gt;

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

&lt;p&gt;You don't need a DevOps engineer to run containers on a VPS. You need localhost-bound ports, Nginx, HTTPS, health checks and a rollback plan. Learn the steps above, then let &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; run them for you. Free 30-day trial, no card.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Deploy Any Web App on a DigitalOcean Droplet: Node, Static or Docker (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:34:39 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/deploy-any-web-app-on-a-digitalocean-droplet-node-static-or-docker-2026-5b6a</link>
      <guid>https://dev.to/vishvajeetshukla/deploy-any-web-app-on-a-digitalocean-droplet-node-static-or-docker-2026-5b6a</guid>
      <description>&lt;p&gt;Most deploy tutorials cover one kind of app. Real servers rarely stay that tidy. A single DigitalOcean Droplet often ends up hosting a Node API, a static marketing site and a container or two.&lt;/p&gt;

&lt;p&gt;This guide shows how to deploy all three on one Droplet, each on its own domain with HTTPS. Then we'll look at how &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; handles the same mix from a desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Droplet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create → Droplets&lt;/strong&gt;, choose a region near your users.&lt;/li&gt;
&lt;li&gt;Image: &lt;strong&gt;Ubuntu LTS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Size: a basic plan is enough to start; you can resize later.&lt;/li&gt;
&lt;li&gt;Authentication: &lt;strong&gt;SSH key&lt;/strong&gt; (not a password).&lt;/li&gt;
&lt;li&gt;Optional: enable the free &lt;strong&gt;Cloud Firewall&lt;/strong&gt; with inbound 22, 80 and 443.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point three DNS records at the Droplet's IP, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api.example.com&lt;/code&gt; for the Node API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;www.example.com&lt;/code&gt; for the static site&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools.example.com&lt;/code&gt; for the Docker app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Base setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@DROPLET_IP
adduser deploy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 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
apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx git certbot python3-certbot-nginx
ufw allow OpenSSH &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  App 1: Node API under PM2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;su - deploy
git clone https://github.com/you/api.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;api
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3001 pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;--&lt;/span&gt; start
pm2 save &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx (&lt;code&gt;/etc/nginx/sites-available/api&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;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.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://127.0.0.1:3001&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="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;
  
  
  App 2: Static site on Nginx
&lt;/h2&gt;

&lt;p&gt;A Vite, Astro or plain HTML site doesn't need a running process at all. Build it and let Nginx serve the files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/you/site.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;site
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build          &lt;span class="c"&gt;# outputs dist/&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/site &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; dist/&lt;span class="k"&gt;*&lt;/span&gt; /var/www/site/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;server_name&lt;/span&gt; &lt;span class="s"&gt;www.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/site&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&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;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No port, nothing to restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  App 3: A Docker app
&lt;/h2&gt;

&lt;p&gt;For anything not built on Node (a Python, Go or PHP service, for example), a container is the cleanest route.&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;-fsSL&lt;/span&gt; https://get.docker.com | sh
git clone https://github.com/you/tool.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tool
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bind the container to localhost only, so it isn't exposed directly:&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;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:8080:8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;server_name&lt;/span&gt; &lt;span class="s"&gt;tools.example.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://127.0.0.1:8080&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="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;
  
  
  Step 3: Enable sites and HTTPS
&lt;/h2&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/&lt;span class="o"&gt;{&lt;/span&gt;api,site,tools&lt;span class="o"&gt;}&lt;/span&gt; /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; api.example.com &lt;span class="nt"&gt;-d&lt;/span&gt; www.example.com &lt;span class="nt"&gt;-d&lt;/span&gt; tools.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's three apps, three runtimes, three Nginx files and one certificate run. Now imagine updating each of them every week and rolling back when one breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same Droplet with ShipOwn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; is a desktop app for Windows, macOS and Linux that deploys to Linux servers over SSH. It installs no deployment agent on your Droplet, and it supports exactly these three targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node apps run by PM2&lt;/strong&gt;: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify and plain Express, with port, health check and restart policy set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static sites served by Nginx&lt;/strong&gt;: Vite with React or Vue, Astro, or plain HTML, published to a release directory. No process, no port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker apps&lt;/strong&gt;: a Dockerfile or Compose file built on the server and published behind Nginx. Images are labelled with the commit they came from, and the previous container is kept until the health check passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each app you go through the same five steps: connect server, pick source, review the editable plan, watch the build, operate. Domains and HTTPS work the same way for all three. ShipOwn writes the virtual host, tests it before reloading, restores it if Nginx rejects it, and requests a Certbot certificate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing existing containers
&lt;/h3&gt;

&lt;p&gt;If the Droplet already runs containers you started by hand, ShipOwn can still help. It inspects each container, maps published and internal ports, shows CPU and memory, flags crash loops, and lets you start, stop or restart a container, open a shell inside it and read its logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping a mixed server healthy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch disk.&lt;/strong&gt; Docker images and old builds add up. &lt;code&gt;docker system df&lt;/code&gt; and &lt;code&gt;du -sh&lt;/code&gt; are your friends. ShipOwn's server view shows disk usage at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One Nginx mistake affects everything.&lt;/strong&gt; Always &lt;code&gt;nginx -t&lt;/code&gt; before reload. ShipOwn does this automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know what's deployed.&lt;/strong&gt; ShipOwn shows which commit each app runs and how far behind its branch it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rolling back on a mixed server
&lt;/h2&gt;

&lt;p&gt;Each app type rolls back differently when you do it by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node app:&lt;/strong&gt; check out the previous commit, rebuild and &lt;code&gt;pm2 reload&lt;/code&gt;. That takes minutes, and the app is in a half-built state while it runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static site:&lt;/strong&gt; keep the previous &lt;code&gt;dist/&lt;/code&gt; folder around and copy it back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker app:&lt;/strong&gt; keep the previous image tag and run it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ShipOwn gives all three the same model. Every deploy goes into its own release directory (or an image labelled with its commit, for Docker). The live pointer only moves after the build and health check pass. A deploy that fails restores the previous release by itself, and you can go back to a retained release later with one click. Database migrations and persistent data still need their own backup and recovery plan, whichever tool you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can ShipOwn deploy a Python or Go app?&lt;/strong&gt; Through Docker, yes. There's no native Python or Go runtime; ship it in a container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ShipOwn need Docker on the server?&lt;/strong&gt; Only for Docker apps. Node and static apps run without it, and ShipOwn can install Docker for you if you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a free trial?&lt;/strong&gt; Yes, 30 days, no card required.&lt;/p&gt;

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

&lt;p&gt;One Droplet can comfortably host an API, a static site and a container. Doing it by hand teaches you how the pieces fit. Doing it every week is where &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; saves time and mistakes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>digitalocean</category>
      <category>docker</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy a Web App on AWS EC2 Without DevOps Headaches (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:32:03 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/deploy-a-web-app-on-aws-ec2-without-devops-headaches-2026-1nme</link>
      <guid>https://dev.to/vishvajeetshukla/deploy-a-web-app-on-aws-ec2-without-devops-headaches-2026-1nme</guid>
      <description>&lt;p&gt;AWS EC2 gives you a virtual server in a huge cloud. That's the good news. The bad news: before your app serves a single request, you're dealing with key pairs, security groups, IP addresses that change on stop and start, and a default user that isn't &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide cuts through it. We'll launch an EC2 instance, deploy a Node.js or Next.js app with PM2 and Nginx, add HTTPS, and then show how &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; turns repeat deploys into a short desktop flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Launch the instance
&lt;/h2&gt;

&lt;p&gt;In the EC2 console, &lt;strong&gt;Launch instance&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMI:&lt;/strong&gt; Ubuntu Server LTS (the default user is &lt;code&gt;ubuntu&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance type:&lt;/strong&gt; a small general-purpose type is fine for most side projects. Small instances can run out of memory during builds; more on that below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key pair:&lt;/strong&gt; create or choose one and download the &lt;code&gt;.pem&lt;/code&gt; file. You won't get it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network settings / Security group:&lt;/strong&gt; allow SSH (22) from &lt;strong&gt;your IP only&lt;/strong&gt;, and HTTP (80) and HTTPS (443) from anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; give yourself enough disk for &lt;code&gt;node_modules&lt;/code&gt;, builds and logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Give it a stable IP
&lt;/h2&gt;

&lt;p&gt;A normal public IP changes when the instance is stopped and started. Allocate an &lt;strong&gt;Elastic IP&lt;/strong&gt; and associate it with the instance, then point your domain's A record at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;400 my-key.pem
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; my-key.pem ubuntu@ELASTIC_IP
&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Install the runtime
&lt;/h2&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; &lt;span class="nt"&gt;-y&lt;/span&gt; git nginx
&lt;span class="c"&gt;# Node.js LTS via NodeSource or nvm&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add swap on small instances.&lt;/strong&gt; &lt;code&gt;next build&lt;/code&gt; and large TypeScript builds can be killed for lack of RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 2G /swapfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="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;h2&gt;
  
  
  Step 5: Deploy the app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/you/app.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;app
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; app &lt;span class="nt"&gt;--&lt;/span&gt; start
pm2 save &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Nginx and HTTPS
&lt;/h2&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;server_name&lt;/span&gt; &lt;span class="s"&gt;app.example.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://127.0.0.1:3000&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-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="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 &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;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-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; app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Certbot can't validate, the usual cause on AWS is the security group: port 80 must be open to the internet for the HTTP challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AWS-specific gotchas
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH times out&lt;/td&gt;
&lt;td&gt;Security group doesn't allow 22 from your current IP&lt;/td&gt;
&lt;td&gt;Update the inbound rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Site works, then dies after a stop/start&lt;/td&gt;
&lt;td&gt;Public IP changed&lt;/td&gt;
&lt;td&gt;Use an Elastic IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Permission denied (publickey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrong user or key&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;ubuntu@&lt;/code&gt; for Ubuntu AMIs and the right &lt;code&gt;.pem&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build killed&lt;/td&gt;
&lt;td&gt;Not enough RAM&lt;/td&gt;
&lt;td&gt;Add swap or build elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;502 Bad Gateway&lt;/td&gt;
&lt;td&gt;App not on the expected port&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm2 logs&lt;/code&gt;, check the port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Repeat deploys with ShipOwn
&lt;/h2&gt;

&lt;p&gt;Launching the instance is a one-time job. Deploying, updating, adding domains and rolling back happen again and again. That's where &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; helps.&lt;/p&gt;

&lt;p&gt;ShipOwn is a desktop app for Windows, macOS and Linux. It connects to your EC2 instance over plain SSH, using the same &lt;code&gt;.pem&lt;/code&gt; key and &lt;code&gt;ubuntu&lt;/code&gt; user. No ShipOwn deployment agent is installed on the instance.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect a server&lt;/strong&gt;: Elastic IP, port 22, user &lt;code&gt;ubuntu&lt;/code&gt;, your key. ShipOwn tests the connection before saving it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a source&lt;/strong&gt;: GitHub repo and branch, or a local folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the plan&lt;/strong&gt;: detected framework, commands, port, domain and target. Every value is editable, and nothing runs until you approve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it build&lt;/strong&gt;: choose a server build, or a &lt;strong&gt;PC build&lt;/strong&gt; that builds on your computer and uploads the verified output. That's handy on a small instance that struggles with &lt;code&gt;next build&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate it&lt;/strong&gt;: live URL, deployed commit, how far behind its branch it is, health, logs, redeploy and rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What ShipOwn takes care of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node apps under PM2&lt;/strong&gt;: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify, Express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static sites on Nginx&lt;/strong&gt;: Vite (React/Vue), Astro, plain HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker apps&lt;/strong&gt; from a Dockerfile or Compose file. It can also install Docker on a server that doesn't have it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx config tested before reload&lt;/strong&gt;, and restored if rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certbot HTTPS&lt;/strong&gt;, with certificate expiry shown in the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release directories and rollback&lt;/strong&gt;: the live pointer only moves after the build and health check pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring without CloudWatch setup
&lt;/h2&gt;

&lt;p&gt;For a single instance, you might not want to set up a full metrics stack. ShipOwn's server view shows CPU, RAM, disk, load and PM2 processes, read directly from &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;pm2 jlist&lt;/code&gt;, next to an embedded terminal. Heartbeats can run from the system tray while the app is running and your PC is awake.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something breaks
&lt;/h2&gt;

&lt;p&gt;Describe the problem to ShipOwn's AI terminal ("the site returns 502 since the last deploy"). Using your own Anthropic, OpenAI or DeepSeek key, it inspects the server and &lt;strong&gt;proposes one command at a time with its reason&lt;/strong&gt;. You approve each one. Destructive commands (recursive deletes, reboots, firewall flushes, stopping SSH) are always held for confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does ShipOwn manage AWS resources like security groups?&lt;/strong&gt; No. You create the instance in AWS. ShipOwn works over SSH once the server exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Amazon Linux?&lt;/strong&gt; ShipOwn handles Ubuntu, Debian, the RHEL family and Alpine. See the &lt;a href="https://shipown.com/docs/supported-environments" rel="noopener noreferrer"&gt;supported environments&lt;/a&gt; page before choosing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-deploy on push?&lt;/strong&gt; Not in ShipOwn. It shows waiting commits, and you click deploy.&lt;/p&gt;

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

&lt;p&gt;EC2 is powerful but has a few traps: security groups, changing IPs, small-instance memory. Get those right once, then use ShipOwn for the deploy-and-rollback loop. &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;Try ShipOwn free for 30 days&lt;/a&gt;, no card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>node</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Deploy a Node.js App on Hetzner Cloud (2026 Guide)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:31:55 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/how-to-deploy-a-nodejs-app-on-hetzner-cloud-2026-guide-53a8</link>
      <guid>https://dev.to/vishvajeetshukla/how-to-deploy-a-nodejs-app-on-hetzner-cloud-2026-guide-53a8</guid>
      <description>&lt;p&gt;Hetzner Cloud is a favourite with developers who want a plain Linux server without a big cloud console in the way. You create a server, get an IP, and SSH in. Everything after that is up to you.&lt;/p&gt;

&lt;p&gt;This guide walks through deploying a Node.js app (Express, NestJS, Fastify or similar) to a Hetzner Cloud server, from the first SSH login to HTTPS on your domain. At the end, we'll do the same deploy with &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; to show which parts can be automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A hardened Ubuntu server on Hetzner Cloud&lt;/li&gt;
&lt;li&gt;Your Node.js app running under PM2&lt;/li&gt;
&lt;li&gt;Nginx proxying your domain to the app&lt;/li&gt;
&lt;li&gt;A free Let's Encrypt certificate&lt;/li&gt;
&lt;li&gt;A simple way to roll back&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the server
&lt;/h2&gt;

&lt;p&gt;In the Hetzner Cloud console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a project, then &lt;strong&gt;Add Server&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pick a location close to your users.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Ubuntu&lt;/strong&gt; (a current LTS release) as the image.&lt;/li&gt;
&lt;li&gt;Pick a small shared-vCPU plan to start. You can resize later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add your SSH public key&lt;/strong&gt; here. Don't rely on the root password email.&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;Firewall&lt;/strong&gt; that allows inbound TCP 22, 80 and 443 only, and attach it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Copy the server's public IPv4 address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: First login and a deploy user
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@SERVER_IP
apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
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;Now test &lt;code&gt;ssh deploy@SERVER_IP&lt;/code&gt; in a new terminal before going further. Then disable password logins in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; (&lt;code&gt;PasswordAuthentication no&lt;/code&gt;) and restart SSH.&lt;/p&gt;

&lt;p&gt;Optional but useful: install &lt;strong&gt;fail2ban&lt;/strong&gt; so repeated failed logins get banned.&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;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Install Node.js, Git and PM2
&lt;/h2&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; &lt;span class="nt"&gt;-y&lt;/span&gt; git nginx
&lt;span class="c"&gt;# install Node.js LTS via NodeSource or nvm&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Get your code onto the server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
git clone https://github.com/you/api.git
&lt;span class="nb"&gt;cd &lt;/span&gt;api
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env    &lt;span class="c"&gt;# set PORT=3000 and secrets&lt;/span&gt;
npm ci
npm run build            &lt;span class="c"&gt;# if you use TypeScript or NestJS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Start with PM2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start dist/main.js &lt;span class="nt"&gt;--name&lt;/span&gt; api    &lt;span class="c"&gt;# or: pm2 start npm --name api -- start&lt;/span&gt;
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;p&gt;&lt;code&gt;pm2 logs api&lt;/code&gt; shows output; &lt;code&gt;pm2 status&lt;/code&gt; shows uptime and restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Nginx reverse proxy
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;/etc/nginx/sites-available/api&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;api.example.com&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;10m&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://127.0.0.1:3000&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="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/api /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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always run &lt;code&gt;nginx -t&lt;/code&gt; first. A typo in one file can stop Nginx serving every site on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: DNS and HTTPS
&lt;/h2&gt;

&lt;p&gt;Point an A record for &lt;code&gt;api.example.com&lt;/code&gt; to the Hetzner IP. Once it resolves:&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;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-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; api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Updates and rollback
&lt;/h2&gt;

&lt;p&gt;The simplest update is &lt;code&gt;git pull &amp;amp;&amp;amp; npm ci &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; pm2 reload api&lt;/code&gt;. The danger: if the build fails halfway, your running code and your files no longer match. A safer layout is &lt;code&gt;releases/&amp;lt;timestamp&amp;gt;&lt;/code&gt; folders plus a &lt;code&gt;current&lt;/code&gt; symlink that you switch only after a successful build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same deploy with ShipOwn
&lt;/h2&gt;

&lt;p&gt;Steps 3 to 8 are the repetitive part. &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; is a desktop app for Windows, macOS and Linux that does them over SSH from your machine. No ShipOwn agent is installed on your Hetzner server.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect a server&lt;/strong&gt;: paste the Hetzner IP, user &lt;code&gt;deploy&lt;/code&gt;, and your key. The connection is tested before it's saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a source&lt;/strong&gt;: your GitHub repo and branch, or a local folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the plan&lt;/strong&gt;: ShipOwn detects the framework (Express, NestJS, Fastify and others) and proposes install, build and start commands, port and domain. Edit anything, then approve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it build&lt;/strong&gt;: logs stream live from the server, or build on your PC and upload the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate it&lt;/strong&gt;: live URL, deployed commit, health, logs, redeploy, rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What it handles for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PM2 process with port, health check and restart policy.&lt;/li&gt;
&lt;li&gt;Nginx virtual host, &lt;strong&gt;tested before reload&lt;/strong&gt;, and restored if Nginx rejects it.&lt;/li&gt;
&lt;li&gt;Certbot certificate when you want HTTPS, with expiry shown in the app.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;release directory per deploy&lt;/strong&gt;, so a failed deploy restores the previous release and you can roll back later.&lt;/li&gt;
&lt;li&gt;fail2ban can be installed and viewed from the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also get a server view with CPU, RAM, disk, load and PM2 processes, read straight from the server, plus an embedded terminal. Nothing extra is installed to collect these metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting on Hetzner
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can't connect on port 80/443&lt;/strong&gt;: check the Hetzner Cloud Firewall as well as &lt;code&gt;ufw&lt;/code&gt; on the server. Both must allow the port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 from Nginx&lt;/strong&gt;: the app isn't listening on the port Nginx expects. Check &lt;code&gt;pm2 logs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EADDRINUSE&lt;/strong&gt;: another process already uses the port. &lt;code&gt;sudo ss -ltnp | grep 3000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;413 Request Entity Too Large&lt;/strong&gt;: raise &lt;code&gt;client_max_body_size&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're stuck, ShipOwn's AI terminal (using your own Anthropic, OpenAI or DeepSeek key) proposes one command at a time with a reason, and you approve each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does ShipOwn work with Hetzner dedicated servers too?&lt;/strong&gt; It targets any Linux server you can reach over SSH: a VPS, a cloud VM or bare metal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Docker?&lt;/strong&gt; No. Node apps run under PM2. Docker apps are supported if you prefer containers.&lt;/p&gt;

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

&lt;p&gt;Hetzner gives you a clean, fast Linux box. Do the setup once by hand so you understand it, then use &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; for repeat deploys, HTTPS and rollback. It has a free 30-day trial, no card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>hetzner</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>7 Heroku Alternatives for Self-Hosting Your App in 2026 (Own the Server)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:30:57 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/7-heroku-alternatives-for-self-hosting-your-app-in-2026-own-the-server-2mh8</link>
      <guid>https://dev.to/vishvajeetshukla/7-heroku-alternatives-for-self-hosting-your-app-in-2026-own-the-server-2mh8</guid>
      <description>&lt;p&gt;Heroku made deployment feel like magic: &lt;code&gt;git push&lt;/code&gt;, wait a minute, get a URL. For a lot of developers that magic is still the benchmark. But more teams now want the same workflow on a server they rent and control, instead of on someone else's platform and bill.&lt;/p&gt;

&lt;p&gt;This guide looks at seven realistic Heroku alternatives for self-hosting in 2026. All of them put your app on a Linux server you own. They differ in how much they install on that server, how much you do by hand, and how you recover when something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want full control and don't mind commands?&lt;/strong&gt; Manual SSH + PM2 + Nginx + Certbot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a web dashboard living on the server?&lt;/strong&gt; Coolify, Dokploy or CapRover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a Heroku-like flow from your desktop, with nothing installed on the server?&lt;/strong&gt; &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a single-purpose deploy script?&lt;/strong&gt; A shell script or a CI job that SSHes in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "Heroku alternative" really means
&lt;/h2&gt;

&lt;p&gt;Heroku gave you four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A build step&lt;/strong&gt; that knows your framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A process manager&lt;/strong&gt; that keeps the app running and restarts it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing and HTTPS&lt;/strong&gt; to a domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases and rollback&lt;/strong&gt; so a bad deploy is one click to undo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any self-hosted replacement has to cover the same four jobs. On a VPS those jobs are usually done by Node or Docker (build), PM2 or containers (process), Nginx and Certbot (routing and HTTPS), and some release-directory scheme (rollback). The question is who wires them together: you, a dashboard on the server, or a tool on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Manual SSH, PM2, Nginx and Certbot
&lt;/h2&gt;

&lt;p&gt;This is the baseline every other option automates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@your-server
git clone https://github.com/you/app.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;app
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; app &lt;span class="nt"&gt;--&lt;/span&gt; start
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/app   &lt;span class="c"&gt;# proxy_pass http://127.0.0.1:3000;&lt;/span&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;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; app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; nothing extra on the server, you learn exactly how it works.&lt;br&gt;
&lt;strong&gt;Bad:&lt;/strong&gt; every deploy repeats these steps, rollback is something you have to invent, and one bad Nginx edit can take down other sites on the same box.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Coolify
&lt;/h2&gt;

&lt;p&gt;Coolify is an open-source self-hosted platform. You install it on your server and use its web dashboard to deploy apps, which run as Docker containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; a Heroku-style dashboard, many one-click services.&lt;br&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; the platform itself runs on your server and uses resources there, and your apps go through Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dokploy
&lt;/h2&gt;

&lt;p&gt;Dokploy is another open-source platform you install on the server. It is also Docker-based and gives you a web UI for applications and databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; modern UI, Docker and Compose focused.&lt;br&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; same model as Coolify, a control plane next to your apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. CapRover
&lt;/h2&gt;

&lt;p&gt;CapRover has been around for years. It installs on your server, uses Docker (Swarm under the hood) and gives you a dashboard plus a CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; mature, simple &lt;code&gt;captain-definition&lt;/code&gt; files.&lt;br&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; Docker is required, and the platform lives on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. ShipOwn (desktop app, no server agent)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt; takes a different approach. It is a desktop app for Windows, macOS and Linux that connects to your Linux server over plain SSH. No ShipOwn deployment agent is installed on the server; it uses the tools that are already there or that it sets up for you.&lt;/p&gt;

&lt;p&gt;The flow mirrors Heroku's:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect a server&lt;/strong&gt;: host, port, user, key or password. The connection is tested before it is saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a source&lt;/strong&gt;: a GitHub repo and branch, or a folder on your computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the plan&lt;/strong&gt;: framework, install/build/start commands, port, domain and target are detected and every value is editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it build&lt;/strong&gt;: on the server or on your PC, with output streaming live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate it&lt;/strong&gt;: live URL, history, health, logs, redeploy and rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What it deploys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node apps under PM2&lt;/strong&gt;: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify and plain Express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static sites on Nginx&lt;/strong&gt;: Vite (React or Vue), Astro or plain HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker apps&lt;/strong&gt;: a Dockerfile or a Compose file. This also covers Python, Go, PHP and other languages, as long as they ship in a container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker is optional. A Node app can run under PM2 on a server with no Docker at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; no control plane on the server, release directories with rollback, Nginx config tested before reload, an AI terminal that proposes one command at a time for your approval.&lt;br&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; no auto-deploy on push (it tells you how many commits a branch is ahead, but a person presses deploy), and it is a commercial app, not open source.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A deploy script or CI job
&lt;/h2&gt;

&lt;p&gt;A GitHub Actions workflow that SSHes in, pulls, builds and restarts PM2 is a perfectly valid Heroku replacement for one app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; auto-deploy on push, free for small usage.&lt;br&gt;
&lt;strong&gt;Bad:&lt;/strong&gt; you still write and maintain the Nginx, HTTPS and rollback parts yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. A managed VPS panel
&lt;/h2&gt;

&lt;p&gt;Classic control panels can host Node apps too, but they are usually aimed at PHP hosting and email. They fit if you already run one; they are rarely the simplest path for a modern JavaScript app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Installs on server&lt;/th&gt;
&lt;th&gt;Docker required&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;th&gt;Dashboard location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual SSH&lt;/td&gt;
&lt;td&gt;Nothing extra&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Your terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coolify&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Web, on server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dokploy&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Web, on server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CapRover&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Web, on server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ShipOwn&lt;/td&gt;
&lt;td&gt;No deployment agent&lt;/td&gt;
&lt;td&gt;No (optional)&lt;/td&gt;
&lt;td&gt;Yes, retained releases&lt;/td&gt;
&lt;td&gt;Desktop app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI script&lt;/td&gt;
&lt;td&gt;Nothing extra&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Ask three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do you want anything running on the server besides your app?&lt;/strong&gt; If not, look at manual SSH, a CI script or ShipOwn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are your apps already containerised?&lt;/strong&gt; If yes, Coolify, Dokploy and CapRover are natural fits. If they are plain Node apps, PM2 is lighter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who fixes it at 2 a.m.?&lt;/strong&gt; If it's you, pick the option where rollback and logs are one click away.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Migrating a Heroku app to a VPS: checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Copy every config var from Heroku into a &lt;code&gt;.env&lt;/code&gt; (or the tool's env editor). Remember that &lt;code&gt;PORT&lt;/code&gt; is now yours to choose.&lt;/li&gt;
&lt;li&gt;Replace Heroku Postgres/Redis add-ons with a managed database or a container on the server, and plan backups.&lt;/li&gt;
&lt;li&gt;Point your domain's A record to the server IP.&lt;/li&gt;
&lt;li&gt;Issue HTTPS with Certbot (or let your tool do it).&lt;/li&gt;
&lt;li&gt;Keep the Heroku app running until the new URL passes a health check, then switch DNS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is self-hosting cheaper than Heroku?&lt;/strong&gt; Often, because one VPS can run several small apps, but you take on maintenance. Measure with your own numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get &lt;code&gt;git push&lt;/code&gt; deploys with ShipOwn?&lt;/strong&gt; Not today. ShipOwn shows that a branch has new commits, and you deploy with one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Docker to leave Heroku?&lt;/strong&gt; No. Node apps run fine under PM2 behind Nginx.&lt;/p&gt;

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

&lt;p&gt;Leaving Heroku doesn't mean going back to hand-written configs. If you like a server-side dashboard, Coolify, Dokploy and CapRover are solid. If you want the Heroku-style flow from your desktop and a clean server, try &lt;a href="https://shipown.com" rel="noopener noreferrer"&gt;ShipOwn&lt;/a&gt;. It starts with a free 30-day trial, and no card is needed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Vishvajeet Shukla&lt;/a&gt;, full stack developer and creator of ShipOwn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>heroku</category>
      <category>selfhosted</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>Vercel vs Firebase for Next.js Hosting: Which Is Best in 2026?</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 06:28:23 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/vercel-vs-firebase-for-nextjs-hosting-which-is-best-in-2026-20ge</link>
      <guid>https://dev.to/vishvajeetshukla/vercel-vs-firebase-for-nextjs-hosting-which-is-best-in-2026-20ge</guid>
      <description>&lt;p&gt;Where you host a Next.js app is more than a DevOps detail: it affects speed, cost, and how much backend you have to build yourself. Two common options are &lt;strong&gt;Vercel&lt;/strong&gt; (built around the Next.js workflow) and &lt;strong&gt;Firebase&lt;/strong&gt; (Google’s app platform with hosting plus many backend services).&lt;/p&gt;

&lt;p&gt;This guide compares them for 2026 projects: marketing sites, dashboards, and products that need auth and data. I am Vishvajeet Shukla, a full-stack developer who ships Next.js sites for Indian and international clients. Related: &lt;a href="https://www.vishvajeetshukla.in/blog/hire-nextjs-developer-in-jaipur-guide" rel="noopener noreferrer"&gt;hire a Next.js developer&lt;/a&gt; · &lt;a href="https://www.vishvajeetshukla.in/blog/core-web-vitals-guide-for-small-business-websites" rel="noopener noreferrer"&gt;Core Web Vitals guide&lt;/a&gt; · &lt;a href="https://www.vishvajeetshukla.in/blog/website-cost-in-jaipur-2026-pricing-guide" rel="noopener noreferrer"&gt;website cost guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick verdict
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick Vercel&lt;/strong&gt; when Next.js features (SSR/SSG/ISR, App Router, previews) are the product surface and you want the least friction deploying frontends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Firebase&lt;/strong&gt; when you want hosting plus a ready ecosystem (Auth, Firestore, Storage, Functions) under one Google cloud umbrella — and you accept more assembly for advanced Next.js rendering modes depending on setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid is normal:&lt;/strong&gt; Next.js on Vercel + Firebase Auth/Firestore, or Firebase for mobile + Next marketing site on Vercel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Firebase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best known for&lt;/td&gt;
&lt;td&gt;Next.js frontend platform&lt;/td&gt;
&lt;td&gt;Backend + hosting suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR / App Router DX&lt;/td&gt;
&lt;td&gt;Excellent, first-class&lt;/td&gt;
&lt;td&gt;Possible; more config tradeoffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview deployments&lt;/td&gt;
&lt;td&gt;Outstanding PR previews&lt;/td&gt;
&lt;td&gt;Available; different workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth / DB included&lt;/td&gt;
&lt;td&gt;Bring your own&lt;/td&gt;
&lt;td&gt;Auth, Firestore, RTDB, Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless functions&lt;/td&gt;
&lt;td&gt;Edge / serverless functions&lt;/td&gt;
&lt;td&gt;Cloud Functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical sweet spot&lt;/td&gt;
&lt;td&gt;Sites, marketing, SaaS UI&lt;/td&gt;
&lt;td&gt;Apps needing BaaS quickly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing shape&lt;/td&gt;
&lt;td&gt;Bandwidth/build/team tiers&lt;/td&gt;
&lt;td&gt;Usage of many products&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Vercel optimises for
&lt;/h2&gt;

&lt;p&gt;Vercel’s mental model is: git push → production, with previews for every PR. Image optimisation patterns, caching, and framework defaults align tightly with Next.js. For SEO-focused business sites, that usually means faster iteration and fewer “why is my SSR broken?” moments.&lt;/p&gt;

&lt;p&gt;You still need a database and auth strategy (Postgres, MongoDB, Supabase, Firebase, etc.). Vercel is not a full backend replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Firebase optimises for
&lt;/h2&gt;

&lt;p&gt;Firebase optimises for shipping product features quickly: sign-in, realtime data, file uploads, push notifications, analytics. Hosting is one piece of a larger kit. Teams that want one vendor for many app services often land here.&lt;/p&gt;

&lt;p&gt;For content-heavy marketing Next.js sites, you must still design rendering, SEO metadata, and caching carefully — Firebase Hosting alone does not magically make a slow app fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js rendering modes and why hosting choice matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static / SSG pages:&lt;/strong&gt; both can serve well with a CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR / dynamic routes:&lt;/strong&gt; platform support and cold starts matter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISR / revalidation:&lt;/strong&gt; developer experience differs by host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge logic:&lt;/strong&gt; useful for geo or auth gates; check product limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your site is mostly static brochure pages, almost any solid host works. If you lean on App Router server components and dynamic data, prefer the platform that documents your exact path clearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing mindset (not a quote sheet)
&lt;/h2&gt;

&lt;p&gt;Both can be cheap at low traffic and expensive when misconfigured. Watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bandwidth and image transformation costs&lt;/li&gt;
&lt;li&gt;Function invocation volume&lt;/li&gt;
&lt;li&gt;Realtime database read/write patterns (Firebase)&lt;/li&gt;
&lt;li&gt;Team seats and commercial plan features&lt;/li&gt;
&lt;li&gt;Egress if you mix multiple clouds poorly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always model your expected monthly active users and peak campaign traffic before locking architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and operations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Environment secrets management&lt;/li&gt;
&lt;li&gt;Role-based access to production projects&lt;/li&gt;
&lt;li&gt;Preview environments that never touch live customer data carelessly&lt;/li&gt;
&lt;li&gt;Backup strategy for databases&lt;/li&gt;
&lt;li&gt;Monitoring for 5xx and function errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ownership matters: your client should control the billing account and project, not a personal freelancers login that vanishes later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Is this mostly a marketing site or a data app?&lt;/li&gt;
&lt;li&gt;Do we need Firebase Auth/Firestore specifically?&lt;/li&gt;
&lt;li&gt;How important are PR preview deployments?&lt;/li&gt;
&lt;li&gt;Who will maintain the stack in 12 months?&lt;/li&gt;
&lt;li&gt;What is the budget ceiling for traffic spikes?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Recommended pairings I often use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business marketing site:&lt;/strong&gt; Next.js on Vercel + forms to email/CRM + WhatsApp CTAs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MVP app:&lt;/strong&gt; Next.js UI + Firebase Auth/Firestore (or Supabase) depending on SQL needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content + product:&lt;/strong&gt; Next.js marketing on Vercel, app subdomain on chosen BaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See also backend tradeoffs in &lt;a href="https://www.vishvajeetshukla.in/blog/supabase-vs-firebase-which-backend-is-right-for-your-next-project" rel="noopener noreferrer"&gt;Supabase vs Firebase&lt;/a&gt; when that post is relevant to your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Choosing Firebase only because “Google” without a data model plan&lt;/li&gt;
&lt;li&gt;Choosing Vercel then inventing a fragile homemade auth in a rush&lt;/li&gt;
&lt;li&gt;Ignoring cold starts on rarely hit serverless routes&lt;/li&gt;
&lt;li&gt;No staging environment&lt;/li&gt;
&lt;li&gt;Leaving projects on a personal free account forever&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I host Next.js only on Firebase?
&lt;/h3&gt;

&lt;p&gt;You can host many Next.js outputs, but advanced rendering workflows are often smoother on Vercel. Validate your exact Next version and features against current Firebase docs before committing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Vercel only for small sites?
&lt;/h3&gt;

&lt;p&gt;No. Teams run large production frontends there. Cost governance and architecture still matter at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about India latency?
&lt;/h3&gt;

&lt;p&gt;Both use global CDNs. Measure TTFB for your audience cities; choose regions/config accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team skills and hiring impact
&lt;/h2&gt;

&lt;p&gt;Your hosting choice should match who will maintain the system. A marketing team that only needs content updates benefits from a boring, well-documented Next.js deploy pipeline. A product team shipping mobile + web features may prefer Firebase’s client SDKs even if the marketing site lives elsewhere.&lt;/p&gt;

&lt;p&gt;When freelancers hand over a project, insist on: repository access, environment variable list, domain DNS notes, and which Google/Vercel project owns production. Orphaned personal accounts are a common disaster six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO implications of hosting
&lt;/h2&gt;

&lt;p&gt;Hosting does not replace content strategy. It does affect TTFB, uptime, HTTPS, redirects, and how easily you ship metadata fixes. Prefer platforms where preview deployments let you QA SEO changes before production. After go-live, connect Search Console and monitor coverage — not only homepage rankings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical architecture recipes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local service business site:&lt;/strong&gt; Next.js on Vercel, form to email, WhatsApp CTAs, optional CMS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalogue exporter:&lt;/strong&gt; Next.js frontend, image CDN, enquiry to CRM; Firebase optional for auth if dealers log in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chatty MVP app:&lt;/strong&gt; Firebase Auth + Firestore, Next.js UI, careful security rules review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL-heavy SaaS:&lt;/strong&gt; Postgres (Supabase or managed SQL) + Next.js; Firebase usually not the core DB.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Migration notes if you switch later
&lt;/h2&gt;

&lt;p&gt;Moving hosts is easier when content lives in git or a CMS, not only in a closed page builder. Keep URLs stable. Export analytics annotations on migration day so you can explain traffic dips. Never change DNS and rendering mode in the same hour without a rollback plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;Write your must-have features (auth, CMS, payments, SSR pages). Map each to a service. Then pick the host that makes the frontend path boringly reliable.&lt;/p&gt;

&lt;p&gt;Need a production Next.js setup? &lt;a href="https://www.vishvajeetshukla.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; · &lt;a href="https://www.vishvajeetshukla.in/projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt; · &lt;a href="mailto:vishvajeet4711@gmail.com"&gt;vishvajeet4711@gmail.com&lt;/a&gt; · more on the &lt;a href="https://www.vishvajeetshukla.in/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://www.vishvajeetshukla.in/blog/vercel-vs-firebase-nextjs-2025" rel="noopener noreferrer"&gt;vishvajeetshukla.in&lt;/a&gt;. Need a developer? &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Hire me&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>vercel</category>
      <category>firebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Node vs Python vs Go for AI Backends (2026)</title>
      <dc:creator>Vishvajeet Shukla</dc:creator>
      <pubDate>Wed, 28 Oct 2026 06:27:54 +0000</pubDate>
      <link>https://dev.to/vishvajeetshukla/node-vs-python-vs-go-for-ai-backends-2026-g34</link>
      <guid>https://dev.to/vishvajeetshukla/node-vs-python-vs-go-for-ai-backends-2026-g34</guid>
      <description>&lt;p&gt;An AI feature is roughly half model prompts and half boring backend work: auth, rate limits, queues, webhooks, and cost control. That is why the stack question — &lt;strong&gt;Node/Express, Python, or Go&lt;/strong&gt; for an AI backend in 2026 — comes up on day one.&lt;/p&gt;

&lt;p&gt;Short answer up front: &lt;strong&gt;Python for heavy AI/RAG experimentation&lt;/strong&gt;, &lt;strong&gt;Node when your product is already JavaScript/TypeScript&lt;/strong&gt;, &lt;strong&gt;Go when you need ultra-efficient service glue at scale&lt;/strong&gt; — not as your first LangChain tutorial language.&lt;/p&gt;

&lt;p&gt;I am Vishvajeet Shukla, a full-stack developer working across Next.js, Node, and AI integrations. Related: &lt;a href="https://www.vishvajeetshukla.in/blog/langchain-rag-ai-agent-for-your-website" rel="noopener noreferrer"&gt;LangChain RAG website agent&lt;/a&gt; · &lt;a href="https://www.vishvajeetshukla.in/blog" rel="noopener noreferrer"&gt;developer blog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table (2026 practical view)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Best default&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LangChain / notebooks / research pace&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Ecosystem depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js site chatbot on same repo&lt;/td&gt;
&lt;td&gt;Node / TS&lt;/td&gt;
&lt;td&gt;One language, simple deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High QPS gateway, low memory&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Efficient concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast MVP for client demo&lt;/td&gt;
&lt;td&gt;Node or Python&lt;/td&gt;
&lt;td&gt;Whichever your team ships faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML training pipelines&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Not even a contest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Node.js + Express (or Next.js route handlers)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same language as React/Next frontends&lt;/li&gt;
&lt;li&gt;Great for streaming responses to the browser&lt;/li&gt;
&lt;li&gt;LangChain.js, Vercel AI SDK, OpenAI official SDKs are solid&lt;/li&gt;
&lt;li&gt;Easy JSON APIs, middleware, auth patterns for web apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch-outs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU-heavy embedding of huge corpora can block the event loop if you do it naively&lt;/li&gt;
&lt;li&gt;Move bulk ingest to workers/queues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Node when:&lt;/strong&gt; your portfolio or SaaS is already TS, and the agent is a product feature not a research lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python (FastAPI / Django Ninja / Flask)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LangChain, LlamaIndex, eval tools, notebooks everywhere&lt;/li&gt;
&lt;li&gt;Fastest path to try retrieval strategies&lt;/li&gt;
&lt;li&gt;Data cleaning and PDF parsing ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch-outs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra service to deploy if the main app is Node&lt;/li&gt;
&lt;li&gt;You must still design production concerns (timeouts, workers, observability)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Python when:&lt;/strong&gt; RAG quality and iteration speed matter more than monorepo purity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple concurrency model, small binaries, predictable performance&lt;/li&gt;
&lt;li&gt;Excellent for API gateways, rate limiters, fan-out proxies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch-outs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI orchestration libraries and examples lag Python/JS&lt;/li&gt;
&lt;li&gt;You will call model HTTP APIs yourself more often&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Go when:&lt;/strong&gt; you already know Go and need a thin, fast control plane — not when learning RAG for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  LangChain workflow: which language?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Learning RAG + agents quickly → &lt;strong&gt;Python&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Shipping on a Next.js portfolio with existing API routes → &lt;strong&gt;Node/TS (LangChain.js)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hardening a multi-tenant gateway later → maybe &lt;strong&gt;Go&lt;/strong&gt; in front of either&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full agent pattern: &lt;a href="https://www.vishvajeetshukla.in/blog/langchain-rag-ai-agent-for-your-website" rel="noopener noreferrer"&gt;LangChain RAG for website AI agents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture patterns that beat language debates
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sync API&lt;/strong&gt; for short chat turns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue + worker&lt;/strong&gt; for re-indexing site content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream tokens&lt;/strong&gt; to UI for perceived speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; frequent answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate&lt;/strong&gt; secrets from the frontend bundle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Language does not fix a missing rate limiter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and cost realities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Model tokens dominate cost more than language runtime for most MVPs&lt;/li&gt;
&lt;li&gt;Retrieval quality dominates answer quality more than framework brand&lt;/li&gt;
&lt;li&gt;Cold starts matter on serverless — warm workers help long RAG chains&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team and hiring angle (India freelancing reality)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;More full-stack freelancers are comfortable in &lt;strong&gt;JS/TS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;AI/ML specialists lean &lt;strong&gt;Python&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go talent is excellent but smaller pool for AI-feature MVPs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ship with the language your maintainer can debug at 11pm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended defaults by project type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Backend pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio chat over blogs&lt;/td&gt;
&lt;td&gt;Node (Next API) + vector DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document-heavy enterprise RAG&lt;/td&gt;
&lt;td&gt;Python worker + any web API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp AI session service&lt;/td&gt;
&lt;td&gt;Node or Python + Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenant AI gateway at scale&lt;/td&gt;
&lt;td&gt;Go edge + Python/Node workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Express server sketch (when Node is right)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// conceptual only&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;post&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/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;Add streaming, timeouts, and abuse controls before public launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python FastAPI sketch (when Python is right)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conceptual only
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My personal recommendation for most website agents
&lt;/h2&gt;

&lt;p&gt;If the product is a &lt;strong&gt;Next.js site&lt;/strong&gt; (like many modern portfolios and SaaS marketing apps): start with &lt;strong&gt;TypeScript + LangChain.js or Vercel AI SDK&lt;/strong&gt;. Spin a &lt;strong&gt;Python service&lt;/strong&gt; only when ingest/eval complexity outgrows the JS tooling.&lt;/p&gt;

&lt;p&gt;Reach for &lt;strong&gt;Go&lt;/strong&gt; when traffic and infrastructure concerns dominate, not when you are still writing the first system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO/AdSense note for developer blogs
&lt;/h2&gt;

&lt;p&gt;Comparison posts like this earn developer traffic when they stay practical and updated. Keep examples honest, avoid fake benchmarks, and link to deeper tutorials. That is also the AdSense-safe way: real teaching, not scraped listicles.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Express outdated for AI APIs?
&lt;/h3&gt;

&lt;p&gt;No. Express (or Fastify) is still fine. Next.js route handlers are also fine. Structure and ops matter more than the framework logo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Python slower than Go for chat APIs?
&lt;/h3&gt;

&lt;p&gt;For typical LLM calls, network and model latency dominate. Language microbenchmarks rarely decide UX.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I mix all three?
&lt;/h3&gt;

&lt;p&gt;Yes — many mature systems do. Start with one to reduce ops pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about Rust?
&lt;/h3&gt;

&lt;p&gt;Great for specialised high-performance components. Overkill for most first AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should freelancers learn first?
&lt;/h3&gt;

&lt;p&gt;If you sell web apps: TypeScript + one RAG path. Add Python when clients need heavier data workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;Tell me your current stack (Next.js, Express, Python, etc.) and whether the agent must live inside the same repo. I will recommend a concrete backend layout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.vishvajeetshukla.in/contact" rel="noopener noreferrer"&gt;Contact form&lt;/a&gt; · &lt;a href="mailto:vishvajeet4711@gmail.com"&gt;vishvajeet4711@gmail.com&lt;/a&gt; · &lt;a href="https://www.vishvajeetshukla.in/projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://www.vishvajeetshukla.in/blog/node-vs-python-vs-go-ai-backends-2026" rel="noopener noreferrer"&gt;vishvajeetshukla.in&lt;/a&gt;. Need a developer? &lt;a href="https://vishvajeetshukla.in" rel="noopener noreferrer"&gt;Hire me&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>python</category>
      <category>go</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
