Node.js Deployment Guide on DigitalOcean Droplet 2026
A practical, step-by-step guide to deploying a production Node.js application on a DigitalOcean Droplet using nvm, PM2, Nginx, and Let's Encrypt.
Deploying a Node.js application to production goes beyond just running npm start — you need a system to keep your app running continuously even when the server reboots, a reverse proxy to manage traffic, and HTTPS to protect data in transit. This guide walks you through every step on a DigitalOcean Droplet, from preparing your instance and installing Node.js via nvm, running your app with PM2, configuring Nginx as a reverse proxy, to setting up free SSL with Let's Encrypt — with every real-world command included.
Contents
Preparing Your Droplet and Installing Node.js (nvm)
Before installing anything, you need to choose a Droplet size that matches your workload. For a small to medium Node.js API or web app, DigitalOcean's Basic Droplet with 1 GiB RAM / 1 vCPU / 25 GB SSD at $6/month is sufficient for testing and low-traffic applications. For production workloads that require build processes or handle many concurrent connections, step up to 2 GiB RAM / 2 vCPU / 60 GB SSD at $18/month so PM2 can run in cluster mode across both cores. If your primary users are in Thailand, select the sgp1 (Singapore) region for the lowest latency, with blr1 (Bangalore) as the next best option. Once you've created your Droplet (choose Ubuntu LTS image), SSH in and update the system first with apt update && apt upgrade -y. Create a non-root user instead of running everything as root with adduser deploy && usermod -aG sudo deploy, then enable DigitalOcean's Cloud Firewall, which is free and lets you restrict access to just SSH (22), HTTP (80), and HTTPS (443). Next, install Node.js via nvm (Node Version Manager) rather than using apt directly, since nvm lets you switch versions easily and always have the latest. Run curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash, then source the updated shell profile with source ~/.bashrc. Install the latest LTS version with nvm install --lts and set it as default with nvm use --lts. Verify the installation with node -v and npm -v. If your machine has only 512 MiB or 1 GiB of RAM, running npm install on large dependency sets may kill the process due to memory exhaustion. To be safe, add a 1-2 GB swap file beforehand.
- Basic Droplet 1 GiB RAM ($6/month) is fine for testing; 2 GiB/2 vCPU ($18/month) recommended for production requiring cluster mode
- Choose region sgp1 (Singapore) for users in Thailand with the lowest latency, or blr1 (Bangalore) as a fallback
- Create a non-root user with adduser + usermod -aG sudo, then enable the free Cloud Firewall to restrict ports to 22/80/443
Running Your Node.js App with PM2
From our hands-on testing — pM2 is a process manager for Node.js that keeps your app running in the background, restarts it automatically if it crashes, and supports running multiple instances in parallel on a single machine. Install it globally with npm install -g pm2. Clone your project repository to the machine with git clone https://github.com/user/myapp.git && cd myapp, then install dependencies with npm install --production. If your project needs to build (like TypeScript or a frontend bundle), run npm run build first. When ready, start your app with pm2 start app.js --name myapp — PM2 will immediately run it as a background process and name it for easy management later. Check the status of all processes with pm2 list, view real-time logs with pm2 logs myapp, or open a dashboard showing CPU and memory with pm2 monit. For Droplets with 2 or more vCPUs, you can use cluster mode to squeeze every core with pm2 start app.js -i max --name myapp, which automatically spawns one worker per CPU core and load-balances requests across them round-robin style. For more complex projects that need environment variables or special arguments, use an ecosystem file instead of typing long commands each time. Create ecosystem.config.js with your name, script, instances, env, and other settings, then run with pm2 start ecosystem.config.js. This approach makes redeployments and migrations to new Droplets consistent because the config lives in one file you can commit to your repository.
- Install PM2 with npm install -g pm2, then start your app with pm2 start app.js --name myapp
- Check status with pm2 list, view real-time logs with pm2 logs, and open the dashboard with pm2 monit
- Droplets with 2+ vCPU cores can use cluster mode with pm2 start app.js -i max to utilize every core
- For complex projects with environment variables or special arguments, use ecosystem.config.js instead of typing commands
- Projects that require TypeScript or frontend builds must run npm run build before pm2 start
Configuring Nginx as a Reverse Proxy
Your Node.js app running under PM2 typically binds to an internal port like 3000, which shouldn't be exposed directly to users. Setting up Nginx as a reverse proxy lets you serve your site on the standard ports 80/443, support multiple apps on the same Droplet using different domains, and simplifies SSL setup in the next step. Install Nginx with apt install nginx -y. Create a new config file at /etc/nginx/sites-available/myapp with a server block like this: server { listen 80; server_name yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } The Upgrade and Connection headers are critical if your app uses WebSocket (like Socket.io), because without them real-time connections will drop or fall back to polling. Enable the config by symlinking it to sites-enabled with ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/. Always test syntax before reloading with nginx -t — if it passes without errors, reload the service with systemctl reload nginx. Don't forget to open the firewall for HTTP/HTTPS with ufw allow 'Nginx Full'. To host multiple apps on the same Droplet, repeat the process by creating separate config files for each domain and pointing each proxy_pass to the different ports your PM2 instances are using.
- Install Nginx with apt install nginx, then create a server block in /etc/nginx/sites-available/
- Set proxy_pass to the internal port where your Node.js app is listening, such as http://localhost:3000
- Include Upgrade and Connection headers if your app uses WebSocket, otherwise real-time connections will drop
- Test syntax with nginx -t before every reload to catch configuration errors before they crash your site
- Open the firewall with ufw allow 'Nginx Full' to allow traffic on ports 80 and 443
Setting Up SSL with Let's Encrypt
After Nginx is proxying correctly over HTTP, the next step is enabling HTTPS with Let's Encrypt, which issues free certificates and auto-renews them. The tool is Certbot, an Nginx-specific plugin. Install it with apt install certbot python3-certbot-nginx -y. Before you run anything, make sure your domain's DNS A record points to your Droplet's IP — if not, Certbot won't be able to verify it. Once DNS is ready, run certbot --nginx -d yourdomain.com -d www.yourdomain.com. Certbot will ask for an email for expiration warnings, then modify your Nginx config automatically, adding a listen 443 block with the certificate path and a redirect from HTTP to HTTPS. Let's Encrypt certificates last 90 days, but Certbot installs a systemd timer or cron job that checks twice daily and auto-renews when expiration gets close (typically under 30 days remaining). Test that auto-renewal works without waiting by running certbot renew --dry-run — if it completes error-free, auto-renewal is ready. One critical detail: port 80 must stay open even after you enable HTTPS, because Let's Encrypt's HTTP-01 challenge verification during renewal needs it. If your firewall blocks port 80 after setting up HTTPS, auto-renewal will silently fail and you won't realize it until the certificate actually expires.
- Install Certbot with apt install certbot python3-certbot-nginx before requesting a certificate
- Ensure your domain's DNS A record points to the Droplet IP before running certbot --nginx
- certbot --nginx -d yourdomain.com -d www.yourdomain.com issues the certificate and updates Nginx config automatically
- Certificates last 90 days and auto-renew via systemd timer — test with certbot renew --dry-run
Auto-restart and Log Management
A point users often miss: one step many people forget after deploying is ensuring PM2 restarts your app when the Droplet reboots, which can happen anytime for maintenance or power issues. After your first pm2 start, always run two companion commands: pm2 startup generates a systemd service directive (copy and run the output with sudo), followed by pm2 save to record the current process list. When the Droplet reboots, systemd will invoke PM2, which loads the saved process list and restarts everything automatically without manual intervention. PM2 stores app stdout/stderr in ~/.pm2/logs/ by default, but if left unmanaged, log files balloon and consume your SSD, especially on smaller Droplets. Install the pm2-logrotate module with pm2 install pm2-logrotate to automatically rotate logs by size or age, compressing old files as .gz. You can configure size limits and retention depth. PM2 auto-restarts crashed processes by default, but if a process restarts too frequently (more than 15 times in 1 minute), PM2 enters an 'errored' state to prevent infinite restart loops — this signals that you need to investigate the actual error in the logs rather than letting it keep failing.
- Run pm2 startup then pm2 save after every deploy to auto-start your app when the Droplet reboots
- PM2 stores logs in ~/.pm2/logs/ by default; without management they can fill your SSD on small Droplets
- Install pm2 install pm2-logrotate to automatically rotate and compress logs
- PM2 auto-restarts crashed processes, but if restarts exceed 15 per minute it enters 'errored' state to prevent loops
When to Use This Approach (Use Cases)
Deploying Node.js on your own Droplet as described here is ideal when you need full control over your infrastructure, such as customizing Nginx config in detail, running multiple apps or services on one machine, managing background workers or cron jobs alongside your web server, or handling large numbers of WebSocket connections that need persistent connections. Sometimes self-hosting offers more flexibility than managed platforms. Another reason is cost: a Basic Droplet starts at $6/month and can run many apps at once if you allocate resources wisely, unlike App Platform which charges per container instance (e.g., $10/month per app for a shared 1vCPU/1GiB container). Conversely, if your team lacks DevOps expertise, doesn't want to manage OS updates, security patches, SSL renewal, or scaling yourself, DigitalOcean App Platform (a managed PaaS) is better. You deploy straight from Git, and everything else — SSL, scaling, zero-downtime updates — is automatic, though you trade flexibility for simplicity and lose OS-level control. In short, Droplet + PM2 + Nginx suits teams with existing DevOps knowledge who need custom infrastructure or multi-service deployments to save costs per app, such as a mid-size SaaS backend, a mobile app API, webhook receivers, or real-time services like chat or notifications. Teams prioritizing speed to market over infrastructure control should consider App Platform instead.
- Ideal for teams needing custom Nginx config, multiple apps/services on one Droplet, or cron/background workers
- Perfect for apps requiring heavy WebSocket persistent connections like chat or notification services
- Long-term cost is often lower when running multiple apps on a single Droplet versus paying per container
- If you lack time to manage servers yourself or prefer not to handle patching and scaling, choose App Platform instead
Common Mistakes and How to Fix Them
The most common mistake is forgetting pm2 startup && pm2 save after deploy, so your app vanishes the moment the Droplet reboots without any warning. Users typically only notice hours later when the site is down. Protect yourself by checking pm2 list after every Droplet maintenance or reboot to confirm processes are still running. The second frequent issue is Nginx not forwarding Upgrade and Connection headers, causing WebSocket apps like Socket.io to fail to connect or drop frequently even though local testing works fine. Check the browser console for WebSocket connection errors. Fix by verifying your Nginx server block includes proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection 'upgrade';. A third problem is EADDRINUSE errors when trying to run a new app on a port that's already in use by a lingering process. Use lsof -i :3000 to find what's holding the port, then kill it or use pm2 delete myapp before starting fresh. A fourth issue is forgetting to open the firewall for Nginx after a fresh install, leaving your site unreachable from outside even though the service is running. Check with ufw status to see if there's a rule allowing Nginx Full or ports 80/443. Finally, certificate auto-renewal fails silently when your firewall or Nginx config blocks port 80 after enabling HTTPS, breaking the Let's Encrypt HTTP-01 challenge. Keep port 80 open at all times even after HTTPS is live, and periodically check certbot certificates to see expiration dates in advance.
- Forgetting pm2 startup && pm2 save leaves your app dead after Droplet reboot — check pm2 list regularly
- Nginx missing Upgrade/Connection headers breaks WebSocket — add them to the proxy config
- Error EADDRINUSE means port conflict — check with lsof -i :PORT and pm2 delete before restarting
Best Practices
For security, disable SSH password login and use SSH keys exclusively, plus create a non-root user for daily work and use sudo only when necessary. Enable DigitalOcean's free Cloud Firewall alongside ufw on the machine to restrict open ports to just what's required. For configuration, never hardcode sensitive values like database passwords or API keys directly in code — use environment variables via a .env file with a library like dotenv, and always add .env to .gitignore to prevent accidental leaks to your repository. For monitoring, DigitalOcean provides free Monitoring and 1 Uptime Check per account — enable them to watch CPU, memory, and disk usage in real-time and get alerts if your site goes down before users do. For backups, create a Droplet snapshot before major changes like upgrading a major Node.js version or altering database schema — snapshots cost only $0.06 per GiB per month, much cheaper than recovering from a failed change. For performance, use PM2 cluster mode on any Droplet with 2+ vCPU cores to harness all cores instead of leaving Node.js single-threaded. For networking, if you need a static IP for whitelisting with external systems, a Reserved IP attached to an active Droplet is free, but if left unattached it costs $5/month — always tie it to your production Droplet to avoid waste.
- Disable SSH password login and use SSH keys only + create a non-root user for daily tasks
- Store sensitive values in .env via dotenv and add .env to .gitignore — never hardcode in code
- Enable the free Cloud Firewall and ufw together, restricting ports to what's truly necessary
- Create Droplet snapshots ($0.06/GiB/month) before major changes in case you need to roll back
- Use PM2 cluster mode on 2+ vCPU Droplets and tie Reserved IPs to active Droplets to avoid the $5/month idle fee