Skip to content
DevOps devops app-deployment 6 min read

Deploying a Static Site

A static site is a website made of plain files (HTML, CSS, JavaScript, images) that a server hands to the browser exactly as they are, with no code running on the server to build each page. Modern frontend frameworks like React, Vue, and Astro compile your source code into this small bundle of static files (often called a “build” or “dist” folder). Deploying one is the simplest, fastest, and cheapest kind of deployment because the server does almost no work. This page shows you how to build that bundle and serve it from your own Ubuntu server with Nginx, plus when a managed host is the smarter choice.

SPA vs MPA — what you are actually deploying

Before deploying, know which kind of site you built.

A SPA (Single Page Application — one HTML file that JavaScript rewrites in the browser as you navigate) loads index.html once, then handles all “pages” with JavaScript. React (Create React App, Vite), Vue, and Angular usually produce SPAs. The catch: if a user reloads /about, the server has no about.html file, so it must be told to fall back to index.html.

A static MPA (Multi Page Application — a real HTML file per route) generates one HTML file per page. Astro, Hugo, and Next.js in static-export mode work this way. These need no special fallback because every URL maps to a real file.

Build typeExample toolsServer needs SPA fallback?
SPAReact (Vite/CRA), Vue, AngularYes — fall back to index.html
Static MPAAstro, Hugo, Next.js exportNo — one file per route

Step 1: Build the frontend

The build runs on your machine (or a CI runner), not on the server. You need Node.js installed to run it. Install it on Ubuntu if it is missing:

sudo apt update
sudo apt install -y nodejs npm
node --version

Output:

v20.18.1

Now, inside your project folder, install dependencies and run the build script:

npm ci
npm run build

npm ci installs the exact versions from package-lock.json (more reliable than npm install for repeatable builds). npm run build runs the build command defined in your package.json. When it finishes, you get an output folder. The name depends on the tool:

FrameworkBuild commandOutput folder
Vite (React/Vue)npm run builddist/
Create React Appnpm run buildbuild/
Astronpm run builddist/
Angularnpm run builddist/<project>/browser/

Output:

vite v5.4.10 building for production...
✓ 342 modules transformed.
dist/index.html                   0.46 kB
dist/assets/index-Dk8s2p.css     12.18 kB
dist/assets/index-Bq9fLm.js     142.71 kB
✓ built in 3.21s

Tip: Most frameworks let you set a “base path” for assets. If your site lives at the domain root (e.g. https://example.com/) the default is fine. If it lives under a sub-path (e.g. /app/), set Vite’s base: '/app/' before building, or every CSS and JS file will 404.

Step 2: Copy the build to the server

Static files belong under /var/www, the conventional Ubuntu location for web content. Create a folder for your site and copy the build into it with scp (secure copy, which copies files over SSH).

From your local machine:

ssh deploy@your-server "sudo mkdir -p /var/www/mysite && sudo chown -R deploy:deploy /var/www/mysite"
scp -r dist/* deploy@your-server:/var/www/mysite/

The first line creates /var/www/mysite and gives ownership to your deploy user so the copy does not need root. The second line copies everything inside dist/ into it. For repeat deploys, rsync is faster because it only transfers changed files:

rsync -avz --delete dist/ deploy@your-server:/var/www/mysite/

--delete removes old files on the server that no longer exist in your build, keeping the folder clean.

Step 3: Configure Nginx

Nginx is a fast web server that will serve your files. Install it and create a site config. A “site config” lives in /etc/nginx/sites-available/ and is switched on by symlinking it into /etc/nginx/sites-enabled/.

sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/mysite

Paste this configuration:

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

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

    # SPA fallback: if a file or folder is not found,
    # serve index.html so client-side routing works.
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache hashed assets aggressively (filenames change on each build).
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Never cache index.html, so users always get the latest app.
    location = /index.html {
        add_header Cache-Control "no-cache";
    }

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}

The key line is try_files $uri $uri/ /index.html. It tells Nginx: try the exact file, then the folder, and if neither exists fall back to index.html. That is what makes SPA routes like /about work on reload. (For a static MPA you can drop the /index.html fallback, but leaving it does no harm.)

The caching rules matter. Build tools put a unique hash in asset filenames (e.g. index-Bq9fLm.js). Because the name changes whenever the content changes, you can cache it for a year safely. But index.html keeps the same name, so it must never be cached, or users would keep loading an old version that points at deleted asset files.

Now enable the site, test the config, and reload:

sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Finally, open the firewall so visitors can reach the site. ufw (Uncomplicated Firewall) is Ubuntu’s simple firewall front-end:

sudo ufw allow 'Nginx Full'

Visit http://example.com and your site should load. To add HTTPS, run sudo certbot --nginx (see the Nginx pages in this section).

When to self-host vs use a managed static host

Serving from your own Nginx server gives you full control and is great when the static site sits beside an API on the same machine. But for a pure frontend with no backend on the box, a managed static host is often simpler and free.

OptionWhen to useWhen NOT to use
Self-hosted NginxYou already run a server; frontend lives next to your APIYou have no server and just need a frontend online
Netlify / Cloudflare PagesPure frontend, want auto-builds from Git, global CDN, free TLSYou need server-side code or a database on the same host

Managed hosts like Netlify and Cloudflare Pages connect to your Git repo, run npm run build for you on every push, and serve the result from a global CDN (Content Delivery Network — servers spread worldwide that cache your files near users) with free HTTPS. There is no server to patch or secure. Choose them unless you specifically need the files served from a machine you control.

Best Practices

  • Build on a CI runner or your laptop, never on the production server — keep the server lean and free of Node tooling.
  • Use npm ci, not npm install, so every build uses the locked dependency versions.
  • Cache hashed /assets for a year but always send index.html with no-cache.
  • Use rsync --delete for deploys so stale files never linger in /var/www.
  • Always run sudo nginx -t before reloading; a broken config can take the site down.
  • Enable gzip (or Brotli) in Nginx to shrink CSS and JS transfers and speed up first load.
  • Add HTTPS with Certbot before going live — plain HTTP is not acceptable in 2026.
Last updated June 15, 2026
Was this helpful?