Skip to main content

Deploying Static Sites on Vessl

Vessl has a dedicated Static build type for sites that produce plain HTML, CSS, and JavaScript files — React SPAs, Vue apps, Astro sites, documentation, landing pages, etc. No Node.js runtime needed at serve time; Vessl packages your build output into an nginx image.

How it works

For static apps, Vessl:

  1. Runs your Build command inside a node:lts-alpine container (with your environment variables available, so VITE_*, NEXT_PUBLIC_*, etc. are baked in).
  2. Takes the files from your Output directory (default: dist).
  3. Packages them into a minimal nginx image.
  4. Serves the image with nginx on port 80 — no PORT variable needed.

There's no running Node.js process at all after deploy. nginx is the only thing serving your site.

Prerequisites

  • A GitHub repository with a frontend project
  • A build script that produces static files (e.g. npm run build)
  • Files land in a predictable output directory (dist, out, build, etc.)

Step 1 — Select Static build type in the wizard

When creating an app:

  1. In the wizard, select Static as the build type (or let Vessl auto-detect it if your repo has index.html, dist/, or out/ at the root).
  2. Set the Build command — for example:
    • Vite: npm run build
    • Astro: npm run build
    • Create React App: npm run build
    • Vue CLI: npm run build
  3. Set the Output directory to match where your build puts files:
FrameworkDefault output directory
Vitedist
Create React Appbuild
Next.js (output: 'export')out
Astrodist
Vue CLIdist
Nuxt (static)dist
  1. Click Deploy.

Step 2 — Environment variables baked at build time

Build-time env vars (like VITE_* for Vite or REACT_APP_* for CRA) are injected into the build container, so they're baked into your JavaScript bundle. Set them in the Environment tab before deploying.

# Example for a Vite app talking to an API
VITE_API_URL=https://api.example.com
VITE_APP_NAME=My App
caution

Environment variables baked at build time are embedded in the JavaScript files served to browsers — don't put secrets or API keys in VITE_* / REACT_APP_* variables.

Step 3 — Client-side routing (SPAs)

If your app uses client-side routing (React Router, Vue Router, etc.), Vessl's nginx config automatically rewrites all requests to index.html so deep links and page refreshes work correctly. You don't need to configure this manually.

Step 4 — Custom domain

Add a custom domain under Settings → Custom Domain. Vessl issues a Let's Encrypt certificate automatically. See Custom domains & SSL for details.

Common issues

Build succeeds but the site shows a blank page Usually the output directory doesn't match. If Vite builds to dist but you set build as the output directory, Vessl can't find the files. Check the build logs to see where your build tool actually puts the output, then update the Output directory setting.

VITE_* environment variable is undefined in the browser The variable wasn't set before the build ran — it's baked in at build time, not at serve time. Set it in the Variables tab, then trigger a fresh deploy.

Old build cached — changes not showing Vessl caches Docker layers between deploys. If you changed an env var but the files look the same to Docker's layer cache, force a fresh build by triggering a new deploy from the Deployments tab. Each deploy produces a new image.

404 on page refresh with client-side routing This is handled automatically by Vessl's nginx config (all paths rewrite to index.html). If you see 404s, make sure you selected Static as the build type — other build types don't inject this nginx config.

Output directory not found after build The error message Output directory 'dist' not found after build means your build command didn't produce files at the expected path. Check the build logs, verify the build command runs successfully locally, and confirm the output directory setting matches the actual output path.