<?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: Nathaniel Toju</title>
    <description>The latest articles on DEV Community by Nathaniel Toju (@fecoded).</description>
    <link>https://dev.to/fecoded</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%2F3911671%2Fb657d4cc-c689-4874-bd45-785f93d71882.jpeg</url>
      <title>DEV Community: Nathaniel Toju</title>
      <link>https://dev.to/fecoded</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fecoded"/>
    <language>en</language>
    <item>
      <title>NestJS Zero-Downtime Deployment on DigitalOcean with GitLab CI/CD and PM2</title>
      <dc:creator>Nathaniel Toju</dc:creator>
      <pubDate>Fri, 03 Jul 2026 17:13:00 +0000</pubDate>
      <link>https://dev.to/fecoded/nestjs-zero-downtime-deployment-on-digitalocean-with-gitlab-cicd-and-pm2-fo7</link>
      <guid>https://dev.to/fecoded/nestjs-zero-downtime-deployment-on-digitalocean-with-gitlab-cicd-and-pm2-fo7</guid>
      <description>&lt;p&gt;Deploying a NestJS backend to production with true zero downtime is more involved than most guides suggest. This is a complete, battle-tested walkthrough — built from real production experience — covering everything from Droplet setup to automated GitLab pipelines that deploy without dropping a single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; DigitalOcean Droplet (Ubuntu 24.04)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; Node.js v18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process Manager:&lt;/strong&gt; PM2 (cluster mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Proxy:&lt;/strong&gt; Nginx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; GitLab CI/CD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy user:&lt;/strong&gt; &lt;code&gt;deployer&lt;/code&gt; (non-root, for security)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1 — Droplet Initial Setup
&lt;/h2&gt;

&lt;p&gt;SSH into your Droplet as root and run the following steps once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Node.js v18
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning&lt;/strong&gt;&lt;br&gt;
Do NOT use &lt;code&gt;nodesource&lt;/code&gt; setup scripts — they may install v20 regardless of the version you specify. Install directly from the official Node.js binary to guarantee the version.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove any existing Node.js installation&lt;/span&gt;
apt-get purge &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs npm
apt-get autoremove &lt;span class="nt"&gt;-y&lt;/span&gt;
apt-get autoclean
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/sources.list.d/nodesource.list
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/sources.list.d/node&lt;span class="k"&gt;*&lt;/span&gt;.list
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/keyrings/nodesource.gpg
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /usr/bin/node /usr/bin/nodejs /usr/bin/npm /usr/bin/npx
apt-get update

&lt;span class="c"&gt;# Download and install Node.js v18 directly from nodejs.org&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://nodejs.org/dist/v18.20.8/node-v18.20.8-linux-x64.tar.xz &lt;span class="nt"&gt;-o&lt;/span&gt; node18.tar.xz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xJf&lt;/span&gt; node18.tar.xz
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; node-v18.20.8-linux-x64/bin/&lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; node-v18.20.8-linux-x64/lib/&lt;span class="k"&gt;*&lt;/span&gt; /usr/local/lib/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; node-v18.20.8-linux-x64/include/&lt;span class="k"&gt;*&lt;/span&gt; /usr/local/include/

&lt;span class="c"&gt;# Verify&lt;/span&gt;
node &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# v18.20.8&lt;/span&gt;
npm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install PM2 and Nginx
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a non-root deploy user
&lt;/h3&gt;

&lt;p&gt;Never deploy as root. Create a dedicated &lt;code&gt;deployer&lt;/code&gt; user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/bash deployer

&lt;span class="c"&gt;# Create app directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/nestapp
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; deployer:deployer /var/www/nestapp

&lt;span class="c"&gt;# Create PM2 log directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/log/pm2
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; deployer:deployer /var/log/pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Set up SSH for the deployer user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/deployer/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/deployer/.ssh
&lt;span class="nb"&gt;touch&lt;/span&gt; /home/deployer/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/deployer/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; deployer:deployer /home/deployer/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure the firewall
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw allow OpenSSH
ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt;
ufw &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2 — Nginx Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the Nginx site config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/nginx/sites-available/nestapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following, replacing &lt;code&gt;your-domain.com&lt;/code&gt; with your actual domain or Droplet IP:&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;upstream&lt;/span&gt; &lt;span class="s"&gt;nestapp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;64&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;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;your-domain.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://nestapp&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="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;240s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&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;/health&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://nestapp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable the site
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Symlink to enable&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/nestapp /etc/nginx/sites-enabled/nestapp

&lt;span class="c"&gt;# Remove default site&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; /etc/nginx/sites-enabled/default

&lt;span class="c"&gt;# Fix file permissions (important — Nginx can't read files owned only by root)&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 /etc/nginx/sites-available/nestapp

&lt;span class="c"&gt;# Test config&lt;/span&gt;
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;

&lt;span class="c"&gt;# Reload Nginx&lt;/span&gt;
systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSL with Certbot
&lt;/h3&gt;

&lt;p&gt;Once your domain DNS is pointing to the Droplet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; your-domain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Make sure &lt;code&gt;dig your-domain.com +short&lt;/code&gt; returns your Droplet IP before running certbot, otherwise the ACME challenge will fail.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 3 — Project File Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ecosystem.config.js
&lt;/h3&gt;

&lt;p&gt;Add this to your project root and commit it to GitLab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nestapp&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;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;node_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--security-revert=CVE-2023-46809&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// remove if not needed&lt;/span&gt;
      &lt;span class="na"&gt;autorestart&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="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;512M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;env_production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/log/pm2/nestapp-out.log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/log/pm2/nestapp-error.log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;log_date_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD HH:mm:ss Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;merge_logs&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="na"&gt;kill_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;listen_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;wait_ready&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="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;h3&gt;
  
  
  Update src/main.ts
&lt;/h3&gt;

&lt;p&gt;Signal PM2 when each worker is ready — this is what makes zero-downtime reload actually work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bufferLogs&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Tell PM2 this worker is ready to receive traffic&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update package.json — disable Husky in production
&lt;/h3&gt;

&lt;p&gt;Without this, &lt;code&gt;npm ci&lt;/code&gt; on the server will fail because Husky tries to install git hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"husky install || true"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  .gitlab-ci.yml
&lt;/h3&gt;

&lt;p&gt;Add this to your project root:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:18-alpine&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;install&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull-push&lt;/span&gt;

&lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH == "main"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_MERGE_REQUEST_IID&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci --prefer-offline&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 hour&lt;/span&gt;

&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run test -- --passWithNoTests&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p release&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cp -r dist package.json package-lock.json ecosystem.config.js release/&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;release/&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 hour&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:latest&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH == "main"&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apk add --no-cache openssh-client rsync bash curl&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eval $(ssh-agent -s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p ~/.ssh &amp;amp;&amp;amp; chmod 700 ~/.ssh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ssh-keyscan -H "$DROPLET_HOST" &amp;gt;&amp;gt; ~/.ssh/known_hosts 2&amp;gt;/dev/null&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Sync build artifacts — .env and node_modules on the server are preserved&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;rsync -avz --delete \&lt;/span&gt;
        &lt;span class="s"&gt;--exclude='.env' \&lt;/span&gt;
        &lt;span class="s"&gt;--exclude='node_modules' \&lt;/span&gt;
        &lt;span class="s"&gt;release/ \&lt;/span&gt;
        &lt;span class="s"&gt;$DROPLET_USER@$DROPLET_HOST:/var/www/nestapp/&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. SSH in, install deps, reload with zero downtime&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;ssh $DROPLET_USER@$DROPLET_HOST &amp;lt;&amp;lt; ENDSSH&lt;/span&gt;
        &lt;span class="s"&gt;set -e&lt;/span&gt;
        &lt;span class="s"&gt;cd /var/www/nestapp&lt;/span&gt;

        &lt;span class="s"&gt;echo "==&amp;gt; Installing production dependencies..."&lt;/span&gt;
        &lt;span class="s"&gt;HUSKY=0 npm ci --omit=dev --prefer-offline&lt;/span&gt;

        &lt;span class="s"&gt;echo "==&amp;gt; Reloading app (zero downtime)..."&lt;/span&gt;
        &lt;span class="s"&gt;pm2 reload ecosystem.config.js --env production --update-env || \&lt;/span&gt;
          &lt;span class="s"&gt;pm2 start ecosystem.config.js --env production&lt;/span&gt;

        &lt;span class="s"&gt;echo "==&amp;gt; Saving PM2 process list..."&lt;/span&gt;
        &lt;span class="s"&gt;pm2 save&lt;/span&gt;

        &lt;span class="s"&gt;pm2 list&lt;/span&gt;
&lt;span class="s"&gt;ENDSSH&lt;/span&gt;

    &lt;span class="s"&gt;# 3. Health check&lt;/span&gt;
    &lt;span class="s"&gt;- |&lt;/span&gt;
      &lt;span class="s"&gt;echo "==&amp;gt; Running health check..."&lt;/span&gt;
      &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="s"&gt;STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://$DROPLET_HOST/ || echo "000")&lt;/span&gt;
      &lt;span class="s"&gt;if [ "$STATUS" = "200" ] || [ "$STATUS" = "201" ]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Deploy successful! (HTTP $STATUS)"&lt;/span&gt;
      &lt;span class="s"&gt;else&lt;/span&gt;
        &lt;span class="s"&gt;echo "Health check failed (HTTP $STATUS) — check pm2 logs"&lt;/span&gt;
        &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
  &lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://$DROPLET_HOST&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4 — SSH Key Setup (GitLab ↔ Droplet)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Generate an SSH key pair on your local machine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"gitlab-deployer"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/gitlab_deployer
&lt;span class="c"&gt;# Leave passphrase empty — CI/CD can't type a passphrase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/gitlab_deployer&lt;/code&gt; — private key (goes into GitLab)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/gitlab_deployer.pub&lt;/code&gt; — public key (goes onto the Droplet)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Add the public key to the Droplet
&lt;/h3&gt;

&lt;p&gt;As &lt;strong&gt;root&lt;/strong&gt; on the Droplet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /home/deployer/.ssh/authorized_keys
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssh-ed25519 AAAA...your-public-key... gitlab-deployer"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/deployer/.ssh/authorized_keys

&lt;span class="c"&gt;# Permissions are critical — wrong permissions = Permission denied every time&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/deployer/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/deployer/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; deployer:deployer /home/deployer/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test it
&lt;/h3&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; ~/.ssh/gitlab_deployer deployer@your-droplet-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should log in without a password prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add variables to GitLab
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;GitLab → Settings → CI/CD → Variables&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Protected&lt;/th&gt;
&lt;th&gt;Masked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contents of &lt;code&gt;~/.ssh/gitlab_deployer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DROPLET_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your Droplet IP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DROPLET_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deployer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DROPLET_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/www/nestapp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt; cannot be masked — GitLab doesn't support masking multiline values. Set it as Protected instead so it's only available on protected branches.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 5 — First-Time Manual Deploy
&lt;/h2&gt;

&lt;p&gt;Before CI/CD can take over, get the app running on the Droplet manually once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and upload from your local machine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build

scp &lt;span class="nt"&gt;-r&lt;/span&gt; dist package.json package-lock.json ecosystem.config.js root@your-droplet-ip:/var/www/nestapp/

ssh root@your-droplet-ip &lt;span class="s2"&gt;"chown -R deployer:deployer /var/www/nestapp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the .env file on the Droplet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As deployer&lt;/span&gt;
nano /var/www/nestapp/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NODE_ENV=production
PORT=3000
# all your other environment variables...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;strong&gt;Important&lt;/strong&gt;&lt;br&gt;
Never commit &lt;code&gt;.env&lt;/code&gt; to GitLab. This file lives only on the Droplet. The rsync command in the pipeline excludes it so it persists safely across every deployment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Start PM2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/nestapp
&lt;span class="nv"&gt;HUSKY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
pm2 start ecosystem.config.js &lt;span class="nt"&gt;--env&lt;/span&gt; production
pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure PM2 to survive reboots
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As deployer&lt;/span&gt;
pm2 startup
&lt;span class="c"&gt;# Copy and run the generated command as root, then:&lt;/span&gt;
pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 6 — Trigger the First Automated Pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add .gitlab-ci.yml ecosystem.config.js package.json
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: add CI/CD pipeline and PM2 config"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to &lt;strong&gt;GitLab → CI/CD → Pipelines&lt;/strong&gt; and watch all 5 stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;install → lint → test → build → deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every push to &lt;code&gt;main&lt;/code&gt; from this point forward triggers a full pipeline and zero-downtime deployment automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7 — How Zero-Downtime Actually Works
&lt;/h2&gt;

&lt;p&gt;PM2 cluster reload works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitLab CI builds the new &lt;code&gt;dist/&lt;/code&gt; and rsyncs it to the Droplet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pm2 reload&lt;/code&gt; sends a graceful signal to worker 0&lt;/li&gt;
&lt;li&gt;Worker 0 finishes all in-flight requests then shuts down&lt;/li&gt;
&lt;li&gt;A new worker 0 boots, loads the new code, sends the &lt;code&gt;ready&lt;/code&gt; signal&lt;/li&gt;
&lt;li&gt;PM2 moves to the next worker and repeats&lt;/li&gt;
&lt;li&gt;Nginx keeps routing traffic to all other online workers throughout&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No requests are dropped. No downtime.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;wait_ready: true&lt;/code&gt; and &lt;code&gt;listen_timeout: 8000&lt;/code&gt; in &lt;code&gt;ecosystem.config.js&lt;/code&gt; are what make this reliable — PM2 waits for the &lt;code&gt;process.send('ready')&lt;/code&gt; signal from &lt;code&gt;main.ts&lt;/code&gt; before it considers the new worker healthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8 — Useful Commands
&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;# Check running processes&lt;/span&gt;
pm2 list

&lt;span class="c"&gt;# View live logs&lt;/span&gt;
pm2 logs nestapp

&lt;span class="c"&gt;# Monitor CPU and memory&lt;/span&gt;
pm2 monit

&lt;span class="c"&gt;# Zero-downtime reload (always use this for deployments)&lt;/span&gt;
pm2 reload nestapp &lt;span class="nt"&gt;--update-env&lt;/span&gt;

&lt;span class="c"&gt;# Hard restart (brief downtime — use only if reload fails)&lt;/span&gt;
pm2 restart nestapp &lt;span class="nt"&gt;--update-env&lt;/span&gt;

&lt;span class="c"&gt;# Reset restart counter&lt;/span&gt;
pm2 reset nestapp

&lt;span class="c"&gt;# View last 50 log lines&lt;/span&gt;
pm2 logs nestapp &lt;span class="nt"&gt;--lines&lt;/span&gt; 50

&lt;span class="c"&gt;# Test Nginx config before reloading&lt;/span&gt;
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;

&lt;span class="c"&gt;# Reload Nginx without downtime&lt;/span&gt;
systemctl reload nginx

&lt;span class="c"&gt;# Test the app is responding on the Droplet&lt;/span&gt;
curl http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&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;&lt;code&gt;ecosystem.config.js not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File not on Droplet yet&lt;/td&gt;
&lt;td&gt;Upload manually via &lt;code&gt;scp&lt;/code&gt; before first CI run&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 SSH folder permissions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chmod 700 ~/.ssh &amp;amp;&amp;amp; chmod 600 ~/.ssh/authorized_keys&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EACCES: permission denied mkdir /node_modules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH heredoc not expanding variables&lt;/td&gt;
&lt;td&gt;Use unquoted &lt;code&gt;ENDSSH&lt;/code&gt; (not &lt;code&gt;'ENDSSH'&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.env deleted on every deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rsync &lt;code&gt;--delete&lt;/code&gt; removes files not in source&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;--exclude='.env'&lt;/code&gt; and sync from a single &lt;code&gt;release/&lt;/code&gt; folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;husky: not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Husky runs &lt;code&gt;npm ci&lt;/code&gt; prepare script on server&lt;/td&gt;
&lt;td&gt;Prefix with &lt;code&gt;HUSKY=0&lt;/code&gt; → &lt;code&gt;HUSKY=0 npm ci --omit=dev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App crash-looping&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;.env&lt;/code&gt; variables&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;pm2 logs nestapp --lines 50&lt;/code&gt; and recreate &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cannot find module dist/main.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rsync uploaded folder contents not the folder&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;dist&lt;/code&gt; without trailing slash in rsync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node version wrong after install&lt;/td&gt;
&lt;td&gt;nodesource installs wrong version&lt;/td&gt;
&lt;td&gt;Install Node directly from &lt;code&gt;nodejs.org&lt;/code&gt; binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PM2 still using old Node version&lt;/td&gt;
&lt;td&gt;PM2 daemon started with old binary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm2 kill&lt;/code&gt; then &lt;code&gt;pm2 start ecosystem.config.js --env production&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx &lt;code&gt;Permission denied&lt;/code&gt; reading config&lt;/td&gt;
&lt;td&gt;File permissions too restrictive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chmod 644 /etc/nginx/sites-available/nestapp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certbot ACME challenge fails&lt;/td&gt;
&lt;td&gt;DNS not pointed to Droplet yet&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;dig your-domain.com +short&lt;/code&gt; — must return Droplet IP first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Built from a real production deployment of a NestJS Fintech backend. Every issue in the troubleshooting table was hit and solved during the actual setup. NB: The node version used here is old; however, you are free to use the latest version&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>devops</category>
      <category>gitlab</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>Securing Your React Native App with FreeRasp: A Practical Implementation Guide</title>
      <dc:creator>Nathaniel Toju</dc:creator>
      <pubDate>Mon, 04 May 2026 09:10:01 +0000</pubDate>
      <link>https://dev.to/fecoded/securing-your-react-native-app-with-freerasp-a-practical-implementation-guide-31na</link>
      <guid>https://dev.to/fecoded/securing-your-react-native-app-with-freerasp-a-practical-implementation-guide-31na</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Building a mobile application that handles sensitive financial data — crypto transactions, KYC verification, gift cards — means security is not an afterthought. It is a core deliverable.&lt;/p&gt;

&lt;p&gt;During the development of a cross-platform fintech application, one of the non-negotiables on the security checklist was &lt;strong&gt;runtime application self-protection (RASP)&lt;/strong&gt;. After evaluating our options, we integrated &lt;strong&gt;FreeRasp&lt;/strong&gt; — an open-source mobile security library — into our React Native codebase.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through what FreeRasp is, why we chose it, how we integrated it into a React Native project, and the key lessons learned along the way.&lt;/p&gt;

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

&lt;p&gt;FreeRasp is an open-source RASP (Runtime Application Self-Protection) SDK maintained by &lt;a href="https://github.com/talsec/Free-RASP-ReactNative" rel="noopener noreferrer"&gt;Talsec&lt;/a&gt;. It provides real-time threat detection for mobile applications by monitoring the environment in which your app is running.&lt;/p&gt;

&lt;p&gt;It detects threats such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rooted or jailbroken devices&lt;/strong&gt; — devices where OS security boundaries have been removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugger attachment&lt;/strong&gt; — active reverse engineering attempts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emulator detection&lt;/strong&gt; — app running in an emulated environment (common in fraud scenarios)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tampering / repackaging&lt;/strong&gt; — modified APKs or IPAs being distributed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unofficial stores&lt;/strong&gt; — app installed from an untrusted source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook frameworks&lt;/strong&gt; — tools like Frida being used to intercept app behaviour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlay attacks&lt;/strong&gt; — malicious apps drawing over your UI to steal input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a fintech app handling real user funds and identity documents, these are not theoretical threats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FreeRasp Over the Alternatives?
&lt;/h2&gt;

&lt;p&gt;We considered a few options:&lt;/p&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;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FreeRasp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source, React Native support, active maintenance, no per-user cost&lt;/td&gt;
&lt;td&gt;Requires configuration per platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appdome&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Comprehensive, no-code&lt;/td&gt;
&lt;td&gt;Expensive, vendor lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom RASP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Enormous dev effort, hard to maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FreeRasp hit the sweet spot for our stage: &lt;strong&gt;production-grade security without the enterprise price tag&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;freerasp-react-native
&lt;span class="c"&gt;# or&lt;/span&gt;
yarn add freerasp-react-native
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS, run pod install after:&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;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Configuration
&lt;/h2&gt;

&lt;p&gt;FreeRasp is configured once — typically in your app's entry point or a dedicated security module. Here is the configuration structure we used:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFreeRasp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setThreatListeners&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;freerasp-react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;androidConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.yourapp.package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;certificateHashes&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-certificate-hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;supportedAlternativeStores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;iosConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;appBundleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.yourapp.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;appTeamId&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_TEAM_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;watcherMail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;security@yourcompany.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isProd&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The &lt;code&gt;certificateHashes&lt;/code&gt; field must match the SHA-256 hash of your app's signing certificate. A mismatch will trigger a tamper alert even in your own build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Handling Threats
&lt;/h2&gt;

&lt;p&gt;This is where the real implementation decision lies. FreeRasp detects threats and calls your handler — but &lt;strong&gt;what you do with that information is entirely your responsibility&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Critical threats — terminate session or block access&lt;/span&gt;
  &lt;span class="na"&gt;rootDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCriticalThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;debugDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCriticalThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;hookDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCriticalThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="c1"&gt;// High severity — warn and limit functionality&lt;/span&gt;
  &lt;span class="na"&gt;emulatorDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleHighThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emulator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tamperDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCriticalThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tamper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="c1"&gt;// Medium severity — log and monitor&lt;/span&gt;
  &lt;span class="na"&gt;unofficialStoreDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleMediumThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unofficialStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;deviceBindingDetected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleMediumThreat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deviceBinding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="c1"&gt;// Informational&lt;/span&gt;
  &lt;span class="na"&gt;passcodeNotSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;promptUserToSetPasscode&lt;/span&gt;&lt;span class="p"&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;handleCriticalThreat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threatType&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="c1"&gt;// Log the event to your backend&lt;/span&gt;
  &lt;span class="nf"&gt;logSecurityEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threatType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Clear sensitive data from state&lt;/span&gt;
  &lt;span class="nf"&gt;clearSensitiveData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Navigate to a blocked screen&lt;/span&gt;
  &lt;span class="nx"&gt;navigationRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SecurityBlock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;threatType&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;
  
  
  Our Threat Response Strategy
&lt;/h2&gt;

&lt;p&gt;We categorised threats into three tiers and responded accordingly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — Block immediately (root, hook, tamper, debug in prod)&lt;/strong&gt;&lt;br&gt;
The user sees a security error screen. No financial actions are possible. The session is terminated and the backend is notified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — Degrade gracefully (emulator, unofficial store)&lt;/strong&gt;&lt;br&gt;
Read-only mode is allowed. Transactions and KYC submission are disabled. The user is informed that full functionality requires a verified device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — Log and monitor (passcode not set, device binding)&lt;/strong&gt;&lt;br&gt;
The user is prompted with a recommendation. Functionality is not blocked but the event is logged for analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Test certificate hashes early&lt;/strong&gt;&lt;br&gt;
We spent time debugging what looked like a tamper detection issue in staging. The root cause was a mismatch between our debug and release certificate hashes. Always configure separate hashes per environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Don't block users silently&lt;/strong&gt;&lt;br&gt;
Your UX around security matters. We initially just crashed the blocked screen with no explanation. We updated it to clearly tell users their device environment isn't trusted and guide them to contact support if they believe it's an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. False positives are real&lt;/strong&gt;&lt;br&gt;
Some older Android devices or development builds can trigger emulator detection unexpectedly. Build a backend feedback mechanism so your security team can review flagged sessions rather than blindly blocking users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Combine with backend validation&lt;/strong&gt;&lt;br&gt;
FreeRasp is a client-side layer. A determined attacker can bypass it. Pair it with server-side checks: JWT binding, IP monitoring, transaction anomaly detection. Defence in depth is the goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. isProd matters&lt;/strong&gt;&lt;br&gt;
Always ensure &lt;code&gt;isProd: true&lt;/code&gt; in your production builds. FreeRasp behaves differently in dev mode and will not enforce certain checks. We added this as a required CI check before any release.&lt;/p&gt;

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

&lt;p&gt;Integrating FreeRasp into our React Native fintech application significantly raised our security baseline without adding meaningful friction to the user experience. For any app handling financial data, identity documents, or crypto assets, runtime protection is no longer optional.&lt;/p&gt;

&lt;p&gt;The library is actively maintained, well-documented, and free — which makes it an easy recommendation for any React Native team working in regulated domains.&lt;/p&gt;

&lt;p&gt;If you have questions about the implementation or want to discuss your own RASP strategy, feel free to reach out or leave a comment below.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nathaniel Toju is a cross-platform mobile and backend engineer based in Lagos, Nigeria, building fintech and healthcare products. He specialises in React Native, NestJS, and secure application architecture.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
