“JavaScript app” can mean a few different beasts: a static front-end (Vite/React, Astro, vanilla), a Node.js server (Express/Fastify/Nest), or a full-stack framework (Next/Nuxt/SvelteKit) that outputs either static assets or a Node server. DigitalOcean supports all of that—either on App Platform (PaaS) or on a Droplet (your own Ubuntu box). I’ll show both paths so you can pick the right tool for the job and not regret a few months from now.
If you want minimal maintenance and automatic HTTPS, use App Platform. If you want full control, predictable performance, and classic Linux ergonomics, use a Droplet with Nginx + PM2. I’ll call out trade-offs along the way.
Choose your path (quick reality check)
- App Platform (PaaS): Connect your GitHub/GitLab repo → pick Node.js or Static Site → deploy. It handles builds, rollbacks, autoscaling tiers, and automatic SSL/TLS on your custom domain. Great default for most projects and teams that prefer “push to deploy”.
- Droplet (Ubuntu server): You manage Node, Nginx, firewall, SSL, processes (PM2/systemd). Slightly more work up front, but you get control, lower steady-state costs at scale, and fewer surprises. Common production pattern: Nginx reverse proxy → PM2-managed Node app → Let’s Encrypt via Certbot.
I’ll start with App Platform (create an account here) because it’s the fastest to production, then show the Droplet pattern that’s stood the test of time.
Path A — App Platform (push-to-deploy, automatic HTTPS)
1) Prepare your repo
Pin your Node version and define explicit build/run scripts so builds are deterministic. App Platform’s Node buildpack respects your engines.node
, and it supports npm, Yarn, or pnpm—pick one and stick to it.
{
"name": "my-app",
"private": true,
"engines": { "node": "22.x" },
"scripts": {
"build": "vite build", // or next build / nuxt build / tsc etc.
"start": "node server.js", // for Node runtime; omit for static sites
"preview": "vite preview"
}
}
If your framework outputs a static site (e.g., Astro, Next export, SvelteKit static), plan to use App Platform’s Static Site component that just serves compiled assets—no runtime needed.
2) Create the app and deploy
- In DigitalOcean, create an App and link your GitHub/GitLab repo. App Platform detects Node or Static Site automatically.
- Set Environment Variables in the component’s Settings.
- For Node apps: supply Build Command (e.g.,
npm ci && npm run build
) and Run Command (e.g.,npm run start
). - Add your custom domain in Settings → Domains and let App Platform provision certs automatically (no Certbot needed).
That’s it—new commits auto-deploy. If you ever wondered “will it handle SSL?”, yes: automatic SSL/TLS with renewals.
3) Optional: CI/CD with GitHub Actions
App Platform can be triggered from Actions so you can run tests/lint/build checks before deploying.
name: deploy-to-digitalocean-app-platform
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
uses: digitalocean/app_action@v2
env:
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_TOKEN }}
APP_ID: ${{ secrets.DO_APP_ID }}
SPEC_PATH: .do/app.yaml
DigitalOcean provides an official how-to and a marketplace Action for this flow.
When App Platform shines: you want speed, built-in HTTPS, rollbacks, and fewer knobs. When it bites: highly stateful or exotic workloads, or when you need very specific OS/network tuning.
Path B — Droplet (Ubuntu 24.04/22.04) with Nginx + PM2 (battle-tested classic)
This is the conservative, reliable setup ops folks have used for years. It scales from hobby to serious traffic if you size the Droplet and tune Nginx.
1) Provision and secure the Droplet
- Create an Ubuntu 22.04/24.04 Droplet.
- Add your SSH key, disable password auth, enable UFW (
OpenSSH
,Nginx Full
). - Keep the box updated (
sudo apt update && sudo apt upgrade -y
).
We have a guide that covers Nginx installation.
2) Install Node.js (LTS) and your app
Use your preferred method (NodeSource, nvm). Then pull your code and install dependencies.
# example with NodeSource (Node 22.x)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs build-essential
# app directory
sudo mkdir -p /var/www/myapp && sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
git clone https://github.com/you/myapp.git .
npm ci
npm run build
3) Run the Node process with PM2 (and make it survive reboots)
sudo npm i -g pm2
pm2 start "npm run start" --name myapp
pm2 save
pm2 startup systemd
4) Put Nginx in front (reverse proxy)
Create a server block that proxies to your Node app on localhost (say your app listens on 127.0.0.1:3000
).
server {
listen 80;
server_name myapp.example.com;
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable it and test:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
5) Add HTTPS with Let’s Encrypt (Certbot)
Point your domain’s DNS A record at the Droplet’s IP first. Then:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.example.com
sudo systemctl status certbot.timer
Certbot edits your Nginx config, installs a valid certificate, and sets up automatic renewal.
Now you’ve got the classic, sturdy stack: Nginx → PM2 → Node, with auto-renewing HTTPS.
Static vs. server-rendered frameworks on DO (what to pick)
- Static (Vite/React static, Astro, Next “export”): On App Platform, use a Static Site component—give it a build command (e.g.,
npm run build
) and a publish directory (dist
,.next/out
,build
). It serves the compiled assets and handles SSL. Dead simple. - Server runtime (Express, Fastify, Next server mode, Nuxt server, SvelteKit w/ Node adapter): On App Platform, choose Node.js (or Docker). On a Droplet, use the Nginx + PM2 pattern above with Certbot for TLS.
Add a proper deployment pipeline (recommended)
Even on App Platform, gate deployments behind tests. On Droplets, a small GitHub Actions job can SSH and restart PM2 on success.
name: deploy-droplet
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
npm ci
npm run build
- name: Deploy over SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DROPLET_IP }}
username: ${{ secrets.DROPLET_USER }}
key: ${{ secrets.DROPLET_SSH_KEY }}
script: |
cd /var/www/myapp
git pull --rebase
npm ci
npm run build
pm2 reload myapp --update-env
For App Platform specific deployments from Actions, use DO’s official workflow/action.
Common gotchas (so you don’t learn the hard way)
- Node version drift: Pin
engines.node
. App Platform’s default moves with LTS—avoid surprise build breaks. - Static routing 404s (Next/SPA): On static sites, ensure framework builds produce correct routes; use framework-specific adapter or redirects if needed. (Typical symptom: works locally, 404 on platform.)
- HTTPS termination location: On App Platform, don’t roll your own HTTPS in Node—let the platform terminate TLS. On Droplets with Nginx, terminate in Nginx and keep Node on HTTP localhost.
- Renewals: Certbot sets up timers; still good to verify
certbot.timer
status quarterly. DigitalOcean’s docs outline renewal behavior and checks.
Final checklists
App Platform checklist
[ ] package.json has engines.node and deterministic scripts
[ ] Repo connected; component type chosen (Static Site or Node.js)
[ ] Build/Run commands set; env vars added in Settings
[ ] Custom domain added; automatic SSL verified
[ ] Optional: GitHub Actions workflow gates deploys
Droplet checklist
[ ] Ubuntu updated; SSH hardened; UFW enabled
[ ] Node LTS installed; app builds locally on server
[ ] PM2 manages the process; pm2 save + startup
[ ] Nginx reverse proxy configured and tested
[ ] Certbot installed; HTTPS working; renew timer active
I’ve anchored this guide to stable primitives—Nginx + PM2 + Certbot on Droplets and buildpack + automatic TLS on App Platform. Those won’t age out next quarter. When DigitalOcean bumps defaults (say Node LTS), your engines.node
pin keeps builds predictable, which is why I hammered on it earlier. If you ever swap frameworks (React → Svelte, Next → Astro), the deployment shapes (static vs Node runtime) still map directly to the same DO components.