<?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: harsh</title>
    <description>The latest articles on DEV Community by harsh (@goose2585).</description>
    <link>https://dev.to/goose2585</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4084849%2Fdef2a066-e0bf-4104-bf75-ac0cdae0ec88.png</url>
      <title>DEV Community: harsh</title>
      <link>https://dev.to/goose2585</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goose2585"/>
    <language>en</language>
    <item>
      <title>AWS EC2 Deployment — Q&amp;A Reference</title>
      <dc:creator>harsh</dc:creator>
      <pubDate>Sat, 22 Aug 2026 18:30:06 +0000</pubDate>
      <link>https://dev.to/goose2585/aws-ec2-deployment-qa-reference-279b</link>
      <guid>https://dev.to/goose2585/aws-ec2-deployment-qa-reference-279b</guid>
      <description>&lt;p&gt;A reference guide compiled from deploying two Node.js/Docker apps  to AWS EC2, covering the real issues hit and how they were fixed.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Getting Connected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I SSH into my EC2 instance?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;400 your-key.pem
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; your-key.pem ubuntu@YOUR_ELASTIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; when asked about the fingerprint the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;chmod 400&lt;/code&gt; doesn't seem to work / I get "bad permissions" / "Permission denied (publickey)"&lt;/strong&gt;&lt;br&gt;
This happens when your &lt;code&gt;.pem&lt;/code&gt; key sits on a Windows drive mounted into WSL (e.g. &lt;code&gt;/mnt/c/Users/you/Downloads&lt;/code&gt;). NTFS doesn't honor Linux permission bits properly. Fix: copy the key into WSL's native filesystem first.&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"/mnt/c/Users/you/Downloads/your-key.pem"&lt;/span&gt; ~/.ssh/your-key.pem
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 ~/.ssh/your-key.pem
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/your-key.pem ubuntu@YOUR_ELASTIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: My key filename has spaces in it — how do I reference it?&lt;/strong&gt;&lt;br&gt;
Wrap it in quotes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"Terminal Key Pair.pem"&lt;/span&gt; ubuntu@YOUR_ELASTIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: How do I know which actual instance/IP I'm connected to?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"http://169.254.169.254/latest/api/token"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token-ttl-seconds: 21600"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://169.254.169.254/latest/meta-data/instance-id
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://169.254.169.254/latest/meta-data/public-ipv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to what the AWS Console shows for your instance — it's easy to accidentally SSH into an old instance if an Elastic IP got reassigned.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Domain Name / HTTPS Without Buying a Domain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: I don't want to buy a domain — can I still get real HTTPS?&lt;/strong&gt;&lt;br&gt;
Yes — use &lt;strong&gt;sslip.io&lt;/strong&gt;. Any hostname like &lt;code&gt;YOUR_IP.sslip.io&lt;/code&gt; automatically resolves to that IP with zero signup. Let's Encrypt (via Certbot) will issue a real, trusted certificate for it just like a paid domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why can't I just use the raw IP with HTTP?&lt;/strong&gt;&lt;br&gt;
Clerk (auth) and Razorpay (payments) both require HTTPS with a real hostname in production/live mode. Plain &lt;code&gt;http://ip&lt;/code&gt; will not work with either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: I later bought a real domain — how do I switch over?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your registrar's DNS panel, add an A record: Host &lt;code&gt;@&lt;/code&gt; → your Elastic IP (add one for &lt;code&gt;www&lt;/code&gt; too if wanted).&lt;/li&gt;
&lt;li&gt;Wait for propagation (&lt;code&gt;nslookup yourdomain.com&lt;/code&gt; should return your IP).&lt;/li&gt;
&lt;li&gt;Update your Nginx &lt;code&gt;server_name&lt;/code&gt; and re-run Certbot with the new domain.&lt;/li&gt;
&lt;li&gt;Update Clerk/Razorpay webhook URLs and CORS settings to the new domain.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  3. Elastic IP
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need an Elastic IP?&lt;/strong&gt;&lt;br&gt;
Yes — without one, your instance's public IP changes on every stop/start, breaking your domain/cert setup. Allocate one (EC2 → Elastic IPs → Allocate) and associate it with your instance (free while attached to a running instance).&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Disk Space
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;docker compose up --build&lt;/code&gt; fails with "no space left on device"&lt;/strong&gt;&lt;br&gt;
Default EC2 root volumes are often only ~8GB (or less after later resizes are lost) — too small for Docker image layers. Check with:&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;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
docker system &lt;span class="nb"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick relief: &lt;code&gt;docker system prune -a -f&lt;/code&gt; (removes unused images/cache, doesn't touch running containers' data).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I permanently fix a too-small disk?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Console → EC2 → Volumes → select the volume → Actions → Modify Volume → increase size (e.g. 25 GiB) → Modify. No downtime.&lt;/li&gt;
&lt;li&gt;On the server, extend the partition:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk                                  &lt;span class="c"&gt;# find your partition name, e.g. nvme0n1p1&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;growpart /dev/nvme0n1 1
&lt;span class="nb"&gt;sudo &lt;/span&gt;resize2fs /dev/nvme0n1p1
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;                                  &lt;span class="c"&gt;# confirm new size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Memory / Swap
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: My Docker build crashes with "JavaScript heap out of memory" or gets silently killed&lt;/strong&gt;&lt;br&gt;
Free-tier EC2 instances (t2/t3.micro) typically have ~1GB RAM — not enough for &lt;code&gt;tsc&lt;/code&gt;/&lt;code&gt;vite build&lt;/code&gt; under load. Fix: add swap space.&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="nb"&gt;sudo chmod &lt;/span&gt;600 /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/swapfile none swap sw 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;fallocate&lt;/code&gt; fails with "No space left on device" while adding swap&lt;/strong&gt;&lt;br&gt;
Your disk is full — fix disk space first (Section 4), then retry the swap commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: A container keeps restarting with a clean "Exited (0)" or crash-looping&lt;/strong&gt;&lt;br&gt;
Check for OOM kills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect CONTAINER_NAME &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.State.OOMKilled}} | RestartCount: {{.RestartCount}}'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &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="s2"&gt;"killed process"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;OOMKilled: true&lt;/code&gt;, add swap. If not, check disk space and container logs for the real error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Node's build step still runs out of memory even with swap&lt;/strong&gt;&lt;br&gt;
Explicitly raise Node's own heap limit in the Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1536 npm run build &lt;span class="nt"&gt;--workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Nginx + Certbot (Reverse Proxy + Free SSL)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Basic Nginx + Certbot setup for a Docker app (server on :5000/5001, client on :5173)?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&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; nginx certbot python3-certbot-nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; YOUR_IP.sslip.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create &lt;code&gt;/etc/nginx/sites-available/yourapp&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;YOUR_IP.sslip.io&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://&lt;/span&gt;&lt;span class="nv"&gt;$host$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;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;YOUR_IP.sslip.io&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/YOUR_IP.sslip.io/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/YOUR_IP.sslip.io/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:5173&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:5001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/socket.io&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:5001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="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;-sf&lt;/span&gt; /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/nginx/sites-enabled/default
&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 start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: Why get the cert with &lt;code&gt;certonly --standalone&lt;/code&gt; before starting Nginx, instead of &lt;code&gt;certbot --nginx&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Because your Nginx config already references the cert files (chicken-and-egg problem) — &lt;code&gt;nginx -t&lt;/code&gt; would fail before the cert exists. Getting it standalone first (with Nginx stopped, freeing port 80) avoids this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I swap a self-signed cert for a real Let's Encrypt one later?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; YOUR_IP.sslip.io
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s|ssl_certificate .*|ssl_certificate /etc/letsencrypt/live/YOUR_IP.sslip.io/fullchain.pem;|'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s|ssl_certificate_key .*|ssl_certificate_key /etc/letsencrypt/live/YOUR_IP.sslip.io/privkey.pem;|'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/server_name _;/server_name YOUR_IP.sslip.io;/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/nginx/sites-available/default
&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 start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: Can I use Caddy instead of Nginx+Certbot?&lt;/strong&gt;&lt;br&gt;
Yes — Caddy auto-issues and renews HTTPS certs with zero manual Certbot commands. A minimal Caddyfile:&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;YOUR_IP.sslip.io&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;handle&lt;/span&gt; &lt;span class="n"&gt;/api/*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5001&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5173&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only run one of Nginx or Caddy at a time — both fight over ports 80/443.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Docker Compose Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;docker compose&lt;/code&gt; vs &lt;code&gt;docker-compose&lt;/code&gt; (hyphen)?&lt;/strong&gt;&lt;br&gt;
Newer Docker installs (&lt;code&gt;docker.io&lt;/code&gt; package) ship the Compose &lt;em&gt;plugin&lt;/em&gt;, invoked as &lt;code&gt;docker compose&lt;/code&gt; (space). The old standalone &lt;code&gt;docker-compose&lt;/code&gt; binary may not exist. If &lt;code&gt;docker-compose&lt;/code&gt; isn't found, use &lt;code&gt;docker compose&lt;/code&gt; instead, or &lt;code&gt;sudo apt install docker-compose-plugin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;permission denied&lt;/code&gt; running docker without sudo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ubuntu
newgrp docker        &lt;span class="c"&gt;# or log out and back in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: &lt;code&gt;.env&lt;/code&gt; file not found even though I created it&lt;/strong&gt;&lt;br&gt;
Check it's actually named &lt;code&gt;.env&lt;/code&gt; (leading dot), not &lt;code&gt;env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; | &lt;span class="nb"&gt;grep env
mv env&lt;/span&gt; .env   &lt;span class="c"&gt;# if needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Q: My frontend deployed but shows a blank page / can't reach the API&lt;/strong&gt;&lt;br&gt;
If your frontend calls a Docker-internal hostname (e.g. &lt;code&gt;http://server:5001&lt;/code&gt;) as its API base URL, that only works &lt;em&gt;inside&lt;/em&gt; Docker's network — a real browser on the user's machine has no idea what &lt;code&gt;server&lt;/code&gt; means. Point your frontend's API base URL at your real public HTTPS URL instead (e.g. &lt;code&gt;https://YOUR_IP.sslip.io/api&lt;/code&gt;).&lt;/p&gt;


&lt;h2&gt;
  
  
  8. WebSocket / Socket.IO Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: WebSocket fails with &lt;code&gt;ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Your frontend is connecting to a different hostname than the one your SSL cert was issued for (e.g. connecting to the raw IP while the cert covers &lt;code&gt;IP.sslip.io&lt;/code&gt;). Make sure the frontend's backend URL env var uses the exact same hostname as your cert and Nginx &lt;code&gt;server_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: WebSocket connects but errors with "Invalid namespace"&lt;/strong&gt;&lt;br&gt;
Socket.IO treats the &lt;em&gt;path&lt;/em&gt; portion of the connection URL as a namespace. If your env var is &lt;code&gt;https://yourhost/api&lt;/code&gt; and your code does &lt;code&gt;io(BACKEND_URL)&lt;/code&gt;, Socket.IO tries to connect to an &lt;code&gt;/api&lt;/code&gt; namespace that doesn't exist on your server. Fix: use a backend URL &lt;strong&gt;without&lt;/strong&gt; any path suffix for the socket connection (just the origin), and append &lt;code&gt;/api&lt;/code&gt; separately only for REST calls.&lt;/p&gt;


&lt;h2&gt;
  
  
  9. MongoDB
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Local Docker Mongo container keeps crash-looping / segfaulting (exit code 139)&lt;/strong&gt;&lt;br&gt;
On tiny EC2 instances (900MB–1GB RAM), running Mongo locally alongside your app is often unstable. Simpler and more reliable: use a free &lt;strong&gt;MongoDB Atlas&lt;/strong&gt; cluster instead of a local container.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Atlas → Network Access → whitelist your EC2's Elastic IP.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;MONGO_URI&lt;/code&gt;/&lt;code&gt;MONGODB_URI&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; to your Atlas connection string.&lt;/li&gt;
&lt;li&gt;Remove the local &lt;code&gt;mongodb:&lt;/code&gt; service from &lt;code&gt;docker-compose.yml&lt;/code&gt; (and any &lt;code&gt;depends_on: mongodb&lt;/code&gt; references).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Q: My Atlas connection string fails to parse&lt;/strong&gt;&lt;br&gt;
If your password contains an &lt;code&gt;@&lt;/code&gt; symbol, it must be URL-encoded as &lt;code&gt;%40&lt;/code&gt;, or the driver misreads the string (it'll look like there are two &lt;code&gt;@&lt;/code&gt; symbols).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mongodb+srv://user:my%40pass@cluster0.xxxxx.mongodb.net/dbname?retryWrites=true&amp;amp;w=majority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. CORS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: API/socket requests get rejected from my live site&lt;/strong&gt;&lt;br&gt;
Your server's &lt;code&gt;CORS_ORIGIN&lt;/code&gt; (or equivalent) env var must exactly match your production URL — scheme, host, and no trailing slash mismatch. E.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;CORS_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://YOUR_IP.sslip.io&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;http://localhost:5173&lt;/code&gt; (leftover dev default) and not missing the &lt;code&gt;.sslip.io&lt;/code&gt; suffix if that's part of your real hostname.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Vite-Specific
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Vite dev server blocks my domain: "Blocked request. This host is not allowed."&lt;/strong&gt;&lt;br&gt;
Vite 5+ blocks unrecognized &lt;code&gt;Host&lt;/code&gt; headers by default in dev mode. Add your hostname to &lt;code&gt;vite.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;allowedHosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_IP.sslip.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_IP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Dead / Legacy Code Breaking Builds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: TypeScript build fails on files that seem unrelated to my app&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;tsc&lt;/code&gt; type-checks everything under your configured &lt;code&gt;include&lt;/code&gt; path (usually all of &lt;code&gt;src/&lt;/code&gt;), even orphaned files nothing imports. If you migrated auth systems, frameworks, etc., leftover dead code can still break the build. Confirm a file/folder is truly unused before deleting:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rln&lt;/span&gt; &lt;span class="s2"&gt;"the-thing-you-suspect"&lt;/span&gt; src &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing outside the suspect file/folder references it, it's safe to delete.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Security / Credential Hygiene
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: I accidentally pasted real secrets (DB password, API keys) somewhere they shouldn't be&lt;/strong&gt;&lt;br&gt;
Treat them as compromised immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB Atlas&lt;/strong&gt;: Database Access → edit user → generate new password → update your connection string everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API keys (Clerk, Razorpay, Gemini, etc.)&lt;/strong&gt;: regenerate from the provider's dashboard, update &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT secrets&lt;/strong&gt;: cheap to rotate, no external account tied to them — just generate new ones:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 48
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Never hardcode secrets directly in &lt;code&gt;docker-compose.yml&lt;/code&gt; — always reference them via &lt;code&gt;env_file: .env&lt;/code&gt; or &lt;code&gt;${VAR}&lt;/code&gt; substitution.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  14. Quick Diagnostic Command Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disk&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
docker system &lt;span class="nb"&gt;df
&lt;/span&gt;docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Memory&lt;/span&gt;
free &lt;span class="nt"&gt;-h&lt;/span&gt;
docker inspect CONTAINER &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.State.OOMKilled}}'&lt;/span&gt;

&lt;span class="c"&gt;# Containers&lt;/span&gt;
docker compose ps &lt;span class="nt"&gt;-a&lt;/span&gt;
docker logs CONTAINER &lt;span class="nt"&gt;--tail&lt;/span&gt; 50
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="c"&gt;# Nginx&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="nb"&gt;sudo &lt;/span&gt;systemctl status nginx &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/nginx/sites-enabled/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Networking / identity&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://yourhost
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"http://169.254.169.254/latest/api/token"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token-ttl-seconds: 21600"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://169.254.169.254/latest/meta-data/instance-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>aws</category>
      <category>docker</category>
    </item>
    <item>
      <title>Shipping a Gumroad Clone to Production</title>
      <dc:creator>harsh</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:03:24 +0000</pubDate>
      <link>https://dev.to/goose2585/shipping-a-gumroad-clone-to-production-58b1</link>
      <guid>https://dev.to/goose2585/shipping-a-gumroad-clone-to-production-58b1</guid>
      <description>&lt;p&gt;This is a walkthrough of the deployment setup behind my Gumroad clone: an EC2 box, Docker Compose, Nginx as the front door, and a GitHub Actions pipeline that does the boring parts so I don't have to SSH in and pray every time I ship a change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shape of it&lt;/strong&gt;&lt;br&gt;
Browser&lt;br&gt;
  │  HTTPS :443&lt;br&gt;
  ▼&lt;br&gt;
Nginx (host)&lt;br&gt;
  ├─► /api/      → gumroad-server container  (:5000)&lt;br&gt;
  ├─► /uploads/  → shared volume on disk&lt;br&gt;
  └─► /          → gumroad-client container  (:5173)&lt;/p&gt;

&lt;p&gt;gumroad-server ─► Redis (cache/sessions)&lt;br&gt;
gumroad-server ─► MongoDB Atlas (or local :27017 fallback)&lt;/p&gt;

&lt;p&gt;Nginx runs on the host, not in a container — it terminates SSL and decides where a request goes before Docker ever sees it. The client and server each live in their own container, and Redis rides along as a third. Mongo isn't containerized in production at all; it's Atlas, with a local instance as a dev/fallback option.&lt;/p&gt;

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

&lt;p&gt;The pipeline: build, ship, prove it's alive&lt;/p&gt;

&lt;p&gt;The GitHub Actions workflow has three jobs that gate each other — no point building Docker images if lint fails, no point deploying if the build fails.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Build &amp;amp; test — install deps, run lint. Fast fail if the code's broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker build — build the server and client images with Buildx, using GitHub Actions cache so rebuilds aren't starting from zero every time. Registry push is stubbed out (push: false) until a registry's wired up — right now the images just prove they can build.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploy — SSH into the box and do the actual work:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;script: |
  &lt;span class="nb"&gt;cd&lt;/span&gt; /home/ubuntu/Gumroad-Clone
  git pull origin main
  docker compose build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; server client
  docker compose down server client
  docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; server client redis

  &lt;span class="nb"&gt;sleep &lt;/span&gt;10

  &lt;span class="nv"&gt;STATUS_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; http://localhost:5000/health&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS_CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 200 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: Backend failed healthcheck with status &lt;/span&gt;&lt;span class="nv"&gt;$STATUS_CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    docker compose logs server
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;gumroad-server npm run seed:products
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deployment completed successfully! 🎉"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to check after every deploy&lt;br&gt;
GET /health → 200 (confirms the DB connection is live, not just that the process started)&lt;br&gt;
GET /api/v1/products/discover → returns the seeded products&lt;/p&gt;

&lt;p&gt;If you're deploying something similar, the whole thing is genuinely just: one host, a reverse proxy, a couple of containers, and a pipeline that refuses to lie to you about whether the deploy worked.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>cicd</category>
      <category>devops</category>
      <category>fullstack</category>
    </item>
  </channel>
</rss>
