Technology Encyclopedia Home >How to Deploy a React App on a Cloud Server — Build, Serve, and Go Live

How to Deploy a React App on a Cloud Server — Build, Serve, and Go Live

React apps feel almost magical to build — hot reload, instant feedback, everything just works. Then you try to put one online and realize you have no idea what the actual deployment process looks like.

The key insight that took me a while to grasp: in production, a standard React app doesn't need Node.js running at all. Running npm run build spits out a dist or build folder full of plain HTML, CSS, and JavaScript. Nginx can serve those files directly — fast, simple, and cheap.

The tricky part is client-side routing. If your app uses React Router, navigating directly to a URL like /profile/123 on a server that doesn't know about those routes will return a 404. I'll show you the two-line Nginx fix for that.

This guide covers building, deploying, handling routing, and HTTPS.

I deploy this on Tencent Cloud Lighthouse running Ubuntu 22.04. For React apps without a backend, the Starter plan (2 GB RAM) handles significant traffic since Nginx is extremely efficient at serving static files — it's mostly idle between requests. For global audiences, I add Tencent Cloud EdgeOne as a CDN layer, which caches assets at edge nodes worldwide and reduces latency for international visitors significantly. Lighthouse's predictable bandwidth pricing (fixed monthly traffic allowance) means no surprise bills even if a post goes viral.


Table of Contents

  1. How React Deployment Works
  2. Prerequisites
  3. Part 1 — Build Your React App
  4. Part 2 — Set Up the Server
  5. Part 3 — Upload the Build to the Server
  6. Part 4 — Configure Nginx to Serve the App
  7. Part 5 — Fix Client-Side Routing
  8. Part 6 — Enable HTTPS
  9. Part 7 — Set Up a Deploy Pipeline (GitHub Actions)
  10. The Gotcha: Blank Page After Reload on a Route
  11. Environment Variables in React

Key Takeaways

  • React production build is static files — Nginx serves them directly, no Node.js needed
  • Add try_files $uri $uri/ /index.html; to Nginx config to fix React Router 404s
  • Environment variables are baked into the bundle at build time (REACT_APP_ or VITE_ prefix)
  • Use GitHub Actions to automate build and deploy on every push to main
  • For global audiences, add EdgeOne CDN in front for edge caching of static assets

How React Deployment Works {#how-it-works}

React builds compile your code into static files:

npm run build
→ /build (CRA) or /dist (Vite)
   ├── index.html
   ├── static/css/main.xxx.css
   └── static/js/main.xxx.js

Nginx serves these files directly. No Node.js process runs in production. This makes React apps cheap to host and easy to scale.

The only complexity: client-side routing. React Router intercepts navigation in the browser, but if someone directly visits /dashboard or refreshes a page, the server needs to return index.html for every path — React Router then takes over. Setting this up in Nginx is the key configuration step.


Prerequisites {#prerequisites}

Requirement Notes
Node.js installed locally For building the app
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
Nginx installed on server See Nginx setup guide
A React project Create React App, Vite, or similar

Part 1 — Build Your React App {#part-1}

On your local machine:

Create React App

# Build for production
npm run build

# The build output is in the /build directory
ls build/
# index.html  asset-manifest.json  static/

Vite

npm run build

# Output is in /dist
ls dist/
# index.html  assets/

The build process minifies and optimizes everything automatically. The build/ or dist/ directory contains everything that needs to go on the server.


Part 2 — Set Up the Server {#part-2}

SSH into your server and create a directory for your app:

ssh ubuntu@YOUR_SERVER_IP

sudo mkdir -p /var/www/myreactapp
sudo chown ubuntu:www-data /var/www/myreactapp
sudo chmod 755 /var/www/myreactapp

Part 3 — Upload the Build to the Server {#part-3}

Option A: SCP from local machine

# On your local machine
scp -r build/* ubuntu@YOUR_SERVER_IP:/var/www/myreactapp/
# For Vite: scp -r dist/* ubuntu@YOUR_SERVER_IP:/var/www/myreactapp/

Option B: GitHub + pull on server

If your repo includes the build or you build on the server:

# On the server
cd ~
git clone https://github.com/your-username/your-react-app.git
cd your-react-app

# Install Node.js if not present
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Install dependencies and build
npm install
npm run build

# Copy build output to web root
cp -r build/* /var/www/myreactapp/
# For Vite: cp -r dist/* /var/www/myreactapp/

Part 4 — Configure Nginx to Serve the App {#part-4}

sudo nano /etc/nginx/sites-available/myreactapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/myreactapp;
    index index.html;

    # Gzip compression for faster loading
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_vary on;

    # Cache static assets aggressively (they have content hashes in filenames)
    location /static/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # All routes fall back to index.html (required for React Router)
    location / {
        try_files $uri $uri/ /index.html;
    }
}
sudo ln -s /etc/nginx/sites-available/myreactapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Visit http://YOUR_SERVER_IP — your React app should load.


Part 5 — Fix Client-Side Routing {#part-5}

This is the configuration that makes React Router work:

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

What this does:

  1. Try to serve $uri (an actual file like /static/js/main.js)
  2. Try to serve $uri/ (a directory)
  3. If neither exists, fall back to /index.html

This means any route (/dashboard, /profile/123, /settings) that doesn't match an actual file gets served index.html, and React Router handles the routing from there.

Without this, a hard refresh or direct URL visit to any route other than / returns a 404.


Part 6 — Enable HTTPS {#part-6}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Test auto-renewal:

sudo certbot renew --dry-run

Part 7 — Set Up a Deploy Pipeline (GitHub Actions) {#part-7}

Manually uploading files gets tedious. Set up GitHub Actions to build and deploy automatically on every push to main:

Create .github/workflows/deploy.yml in your repo:

name: Deploy React App

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install and build
        run: |
          npm ci
          npm run build

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "build/"
          target: "/var/www/myreactapp"
          strip_components: 1

Add secrets in your GitHub repo settings:

  • SERVER_IP = your server's public IP
  • SSH_PRIVATE_KEY = your private SSH key

Every push to main now builds and deploys automatically.


The Gotcha: Blank Page After Reload on a Route {#gotcha}

You navigate to /dashboard in React — looks great. You refresh the page — blank page or 404.

This is the React Router + static server problem. The fix is the try_files $uri $uri/ /index.html; line in Nginx. If you're seeing a blank page on refresh, check:

# Verify your Nginx config has the try_files line
grep -A 3 "location /" /etc/nginx/sites-enabled/myreactapp

If the line is there but it's still not working, check the browser console for JavaScript errors — it might be a different issue (wrong base URL, missing env variables, etc.).

Also common: if your app is deployed at a subdirectory (e.g., yourdomain.com/app/), you need to set homepage in package.json:

{
  "homepage": "/app"
}

Environment Variables in React {#env-vars}

React environment variables are embedded at build time — they're not available at runtime like Node.js env vars. Create a .env.production file before building:

# .env.production
REACT_APP_API_URL=https://api.yourdomain.com
REACT_APP_ANALYTICS_ID=your-analytics-id

In your code:

const apiUrl = process.env.REACT_APP_API_URL;

Important: Only variables prefixed with REACT_APP_ are included in the build. All REACT_APP_ variables are visible in the browser — never put secrets in them.

For Vite, variables are prefixed with VITE_:

# .env.production
VITE_API_URL=https://api.yourdomain.com
const apiUrl = import.meta.env.VITE_API_URL;

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

Do I need Node.js running in production for a React app?
Not for a standard Create React App or Vite build. npm run build generates static HTML, CSS, and JavaScript files that Nginx serves directly — no Node.js process needed in production.

What causes 404 errors when refreshing a React page directly?
React Router handles navigation in the browser, but the server doesn't know about those routes. When you refresh /profile/123, Nginx tries to find that file and fails. Fix: add try_files $uri $uri/ /index.html; to your Nginx config.

How do I deploy React app updates?
Run npm run build locally, then upload the new build/ or dist/ folder to your server (or automate with GitHub Actions). The deployment is just replacing static files.

What's the difference between npm run build and npm start?
npm start runs the development server with hot reload — not for production. npm run build creates an optimized, minified production bundle in the build/ or dist/ directory.

How do I configure environment variables for a React production build?
Create .env.production in your project root. Variables must start with REACT_APP_ (CRA) or VITE_ (Vite) to be included in the build. They're baked into the bundle at build time.

Deploy your React app today:
👉 Tencent Cloud Lighthouse — Nginx-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers