react-build-deployment

React Build and Deployment on a VPS

  • 6 min

Congratulations! 🎉 If you’ve made it this far, you have a fully functional React application running on your computer (localhost). You can navigate, fetch data, it has styles, and it’s optimized.

But there’s a problem (a tiny one… nah… no big deal): only you can see it 🤔.

The final step of any software development is Deployment: uploading our application to the internet so the rest of the world can use it.

But before uploading anything, we need to understand that the code we wrote is not the code the browser will ultimately execute. We need to do the Build.

Development vs Production

When we run npm run dev, Vite starts a development server designed for us, the programmers:

  • It has Hot Module Replacement (HMR) to see instant changes.
  • Error messages are detailed.
  • The code is “loose” (hundreds of files) for easy debugging.

This is great for programming, but it’s not what we want to serve to the end-user. It’s slower, heavier, and designed for debugging.

For the user, we want:

  • A single compacted (Minified) JS file.
  • Optimized code (Tree Shaking, Code Splitting).
  • No heavy debugging tools.

The npm run build command

To generate this optimized version, we go back to the terminal, stop the development server (Ctrl+C), and run:

npm run build
Copied!

You’ll see Vite start working and, in a few seconds, it will show you a summary of the generated files.

The dist folder

If you look at your project, you’ll see a new folder called dist (distribution).

This folder is your application.

Everything in src, node_modules, etc., no longer matters. Vite has taken all your code, translated, compressed, and packaged it inside dist.

Inside, you’ll generally see:

  • index.html: The entry point.
  • assets/: Your JS and CSS with strange names (hashes) to avoid cache problems (e.g., index-XyZ123.js).

Do not try to open index.html by double-clicking it. If you do, you’ll see a blank screen. Modern applications need to be served via the HTTP protocol, not the file protocol.

Deploying on a VPS

A React application built with Vite is, ultimately, a static website. We don’t need Node.js running in production to serve it. We need a web server, like Nginx, serving the dist folder.

Let’s start with the option that gives us the most control: your own VPS.

A VPS is essentially a server (generally Linux) rented from a provider (Hetzner, OVH, DigitalOcean, Linode, whichever you prefer).

The important difference is that here, we don’t give our application to a platform to decide for us. We serve the files ourselves.

What we need

For this deployment, we need:

  • A VPS with Linux (Ubuntu Server, for example).
  • SSH access.
  • A domain pointing to the server.
  • Nginx to serve the files.
  • Certbot to activate HTTPS.

The idea will be this:

React + Vite
    ↓ npm run build
dist/
    ↓ upload to VPS
/var/www/mi-app/
    ↓ Nginx
https://midominio.com
Copied!

Prepare the server

Log in via SSH to your VPS:

ssh usuario@IP_DEL_SERVIDOR
Copied!

Update packages and install Nginx:

sudo apt update
sudo apt install nginx
Copied!

Create a folder for your application:

sudo mkdir -p /var/www/mi-app
sudo chown -R $USER:$USER /var/www/mi-app
Copied!

I’m using /var/www/mi-app because it’s a typical location for websites on Linux. You can use another path, but try not to scatter projects in strange folders. Otherwise, no one will remember where anything is.

Upload the dist folder

We have two common options:

  • Build locally and upload the dist folder.
  • Upload the code to the VPS and run npm run build there.

For a static React app, building locally and uploading dist is perfectly valid. From your computer, after running npm run build, you can use rsync:

rsync -avz --delete dist/ usuario@IP_DEL_SERVIDOR:/var/www/mi-app/
Copied!

The --delete flag makes the server match your dist folder exactly. If you’ve deleted an old file, it will also be deleted on the VPS.

Double-check the path before using --delete. It’s very useful, but also very obedient. If you tell it to delete something it shouldn’t, it will do so without question.

If you’re on Windows and don’t have rsync, you can use scp or any SFTP client.

scp -r dist/* usuario@IP_DEL_SERVIDOR:/var/www/mi-app/
Copied!

On Windows, you can also use WSL, which is quite recommended.

Configure Nginx

Now let’s tell Nginx to serve our application. Create a configuration file:

sudo nano /etc/nginx/sites-available/mi-app
Copied!

Basic content:

server {
    listen 80;
    server_name midominio.com www.midominio.com;

    root /var/www/mi-app;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /assets/ {
        try_files $uri =404;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
Copied!

Enable the site:

sudo ln -s /etc/nginx/sites-available/mi-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Copied!

The important line is try_files $uri $uri/ /index.html;. If the browser requests /contacto and there is no real file named contacto, Nginx returns index.html. Then, React Router decides which component to render.

Update the application

Every time you want to publish a new version:

npm run build
rsync -avz --delete dist/ usuario@IP_DEL_SERVIDOR:/var/www/mi-app/
Copied!

And that’s it. No need to restart Node, start processes, or maintain PM2. Nginx is serving static files.

If you want to automate this, you can create a small deploy.sh script with the build and the rsync command. There’s no need to set up a whole platform to start deploying comfortably.