[ Back to log ]

Deploying a TypeScript Express.js app on a VPS

Node.js

TypeScript adds an extra compilation step to your Express deployments. You can’t just run node index.js right out of the repo. You need to build the project, manage the background process, and set up a reverse proxy.

1. Prepare your application

Make sure your build scripts actually output to the right place before you push to the server.

TypeScript configuration

Check that your tsconfig.json outputs the compiled code to a specific directory (like dist):

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Package.json scripts

You need standard build and start scripts:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node-dev src/index.ts"
  }
}

2. Server setup

SSH into your server and install Node.js. The NodeSource setup script is the easiest way to get the latest LTS version:

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

3. Clone and build your app

Create a directory, clone your repo, and compile the code:

sudo mkdir -p /var/www/your-express-app
sudo chown -R $USER:$USER /var/www/your-express-app
cd /var/www/your-express-app

git clone your-repository-url .

npm install
npm run build

Environment setup

Set up your environment variables. It’s important to set NODE_ENV to production so Express knows to cache views and drop verbose error messages.

cp .env.example .env
nano .env

4. Install and configure PM2

Node is single-threaded and crashes if an unhandled exception occurs. PM2 restarts your app automatically if it goes down.

Install it globally:

sudo npm install -g pm2

Start your compiled app:

pm2 start dist/index.js --name "your-app-name"

Tell PM2 to boot up automatically when the server restarts:

pm2 startup
pm2 save

(If you ever need to check on things, pm2 logs and pm2 monit are your best friends).

5. Configure Nginx as a reverse proxy

You shouldn’t expose your Node app directly to port 80. Use Nginx to handle the incoming connections and pass them to Express.

Create a new Nginx config file:

sudo nano /etc/nginx/sites-available/your-express-app

Add this configuration (adjust the port if your app doesn’t use 3000):

server {
    listen 80;
    server_name your-domain.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_cache_bypass $http_upgrade;
    }
}

Enable it and restart Nginx:

sudo ln -s /etc/nginx/sites-available/your-express-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

6. Automate updates

Write a quick bash script so you don’t have to remember these steps every time you deploy.

nano /var/www/your-express-app/deploy.sh
#!/bin/bash
cd /var/www/your-express-app

git pull
npm install
npm run build
pm2 restart your-app-name --update-env

Make it executable:

chmod +x deploy.sh

Now you just run ./deploy.sh to push updates live.

Production checklist

Before you call it done, double-check a few things:

  • NODE_ENV is actually set to production.
  • You aren’t shipping source maps to production unless you have a specific reason to.
  • You have some form of rate limiting (like express-rate-limit).
  • You’ve set up security headers (like helmet).