Deploying a React app on an Ubuntu VPS gives you control over performance, caching, domains, and release timing. The reliable production pattern is simple: build the app into static files, serve those files with Nginx, add HTTPS, and make every release reversible.
This guide walks through that pattern for a React single-page application built with Vite. It also covers client-side routing, cache headers, DNS, firewall rules, certificate renewal, and a low-risk update workflow.
What you will deploy
A browser does not run your React source files directly. Your build tool transforms the source into production-ready HTML, CSS, JavaScript, and assets. Vite writes that output to the dist directory by default, producing a bundle suitable for static hosting.
Nginx will serve those files directly. You do not need to keep a Node.js process running for a purely client-rendered React application. If your project uses server-side rendering, API routes, or a framework server, use a reverse-proxy deployment instead.
- Application: React with a production build script
- Server: Ubuntu VPS with a public IP address
- Web server: Nginx
- TLS: a trusted certificate managed by Certbot
- DNS: a domain or subdomain pointed at the VPS
1. Prepare DNS and the Ubuntu server
Create an A record for your domain that points to the VPS IPv4 address. Add an AAAA record only when IPv6 is configured and reachable on the server. DNS changes can take time to propagate, so verify the record before requesting a certificate.
Connect over SSH using a non-root account with sudo access, then install pending security updates and Nginx:
sudo apt update
sudo apt upgrade -y
sudo apt install nginx -y
Check that Nginx started successfully:
sudo systemctl status nginx --no-pager
sudo nginx -t
If UFW is enabled, allow web traffic before continuing:
sudo ufw allow 'Nginx Full'
sudo ufw status
Keep SSH allowed before enabling or changing a firewall. A firewall rule mistake can lock you out of the VPS.
2. Build the React app for production
Build in a trusted development or CI environment. A reproducible build starts from the lock file, so use npm ci when the project has package-lock.json.
git pull --ff-only
npm ci
npm run build
For a standard Vite project, the compiled site is now in dist. Preview the build before deployment:
npm run preview
Open the preview URL and test the home page, navigation, forms, and important deep links. Build-time environment variables are embedded in the client bundle, so never place database passwords, API secrets, or private credentials in variables exposed to browser code.
If the application will live under a subdirectory instead of the domain root, set Vite's base option before building. Otherwise, generated asset paths may point to the wrong location.
3. Create a release directory
Use versioned release folders and a current symbolic link. This makes rollback faster than copying files over the live directory.
sudo mkdir -p /var/www/example.com/releases
sudo chown -R $USER:www-data /var/www/example.com
release=$(date +%Y%m%d%H%M%S)
mkdir -p /var/www/example.com/releases/$release
rsync -av --delete dist/ /var/www/example.com/releases/$release/
ln -sfn /var/www/example.com/releases/$release /var/www/example.com/current
Replace example.com with your real domain. Confirm that the web server can read the files:
find /var/www/example.com/current -type d -exec chmod 755 {} \;
find /var/www/example.com/current -type f -exec chmod 644 {} \;
Avoid making the site directory world-writable. Nginx needs read access, not ownership of your application source or deployment credentials.
4. Configure Nginx for React Router
Create a server block:
sudo nano /etc/nginx/sites-available/example.com
Use the following starting point:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/current;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
The try_files fallback is essential for a single-page application. A request for /account/settings does not correspond to a physical file, so Nginx returns index.html and lets the client-side router handle the URL. Static assets still return a real 404 when missing.
Vite normally generates fingerprinted asset filenames, which are safe to cache for a long time. Keep index.html uncached so visitors discover new asset filenames after a release.
Enable the site and validate the configuration before reloading Nginx:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx
Do not reload when nginx -t reports an error. Fix the named file and line first.
5. Add HTTPS with Certbot
Wait until the domain resolves to this server and port 80 is reachable. Certbot's Nginx plugin can complete the HTTP challenge and update the server block to use the issued certificate.
Use the current installation method from the official Certbot instructions for your Ubuntu release. After installation, request the certificate:
sudo certbot --nginx -d example.com -d www.example.com
Choose the HTTPS redirect when prompted. Then verify both the certificate and automatic renewal:
sudo certbot certificates
sudo certbot renew --dry-run
A failed dry run is not something to ignore. Check DNS, firewall access, Nginx configuration, and the renewal logs before relying on unattended renewal.
6. Test the production deployment
Run a short acceptance check from a private browser window and from the command line:
curl -I https://example.com
curl -I https://example.com/account/settings
- HTTP redirects to HTTPS.
- The certificate covers every hostname you serve.
- The home page loads without console errors.
- A deep link loads directly instead of returning 404.
- Refreshing a client-side route still works.
- Hashed JavaScript and CSS files have long cache headers.
index.htmlis not cached for a year.- API requests target the intended production endpoint.
Also check the Nginx logs while testing:
sudo tail -f /var/log/nginx/example.com.access.log /var/log/nginx/example.com.error.log
7. Deploy updates without avoidable downtime
For each update, create a new release directory, upload the complete build, run basic checks, and only then move the current link. The switch is fast and does not expose visitors to a partially copied build.
release=$(date +%Y%m%d%H%M%S)
mkdir -p /var/www/example.com/releases/$release
rsync -av --delete dist/ /var/www/example.com/releases/$release/
ln -sfn /var/www/example.com/releases/$release /var/www/example.com/current
sudo nginx -t && sudo systemctl reload nginx
Retain at least one known-good release. If monitoring shows a problem, point current back to the previous folder and reload Nginx. Keep releases only as long as your rollback policy requires so old builds do not consume disk space indefinitely.
Common React deployment problems
Deep links return 404
The Nginx location block is missing the /index.html fallback, or another location block takes precedence. Test the final configuration with sudo nginx -T.
JavaScript or CSS files return 404
Check the Vite base path, the Nginx root, and the contents of the active release. Use the browser Network panel to see the exact requested URL.
Visitors keep seeing the old version
Do not apply a long immutable cache policy to index.html. Fingerprinted assets can be cached; the HTML entry point should revalidate.
HTTPS issuance fails
Confirm that the A and AAAA records resolve correctly, ports 80 and 443 are open, and no proxy or stale IPv6 record sends validation traffic elsewhere.
The app exposes a secret
Anything delivered to a browser must be treated as public. Move privileged operations and secret-bearing API calls to a server-side service.
Production checklist
- Use a lock file and a repeatable production build.
- Point verified DNS records to the correct VPS.
- Serve only the compiled output, not the source repository.
- Configure SPA fallback without masking missing static assets.
- Test Nginx before every reload.
- Enable HTTPS and test certificate renewal.
- Cache hashed assets and revalidate the HTML entry point.
- Keep logs, uptime checks, backups, and a rollback release.
Manage the workflow from one control panel
A production deployment includes more than copying files. Teams also need domains, SSL, services, logs, monitoring, backups, and a consistent rollback process. Explore Core Panel's server management features, review the pricing and trial options, or compare the operational model in our guide to evaluating a cPanel alternative.



