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:
- Runs your Build command inside a
node:lts-alpinecontainer (with your environment variables available, soVITE_*,NEXT_PUBLIC_*, etc. are baked in). - Takes the files from your Output directory (default:
dist). - Packages them into a minimal nginx image.
- 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:
- In the wizard, select Static as the build type (or let Vessl auto-detect it if your repo has
index.html,dist/, orout/at the root). - 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
- Vite:
- Set the Output directory to match where your build puts files:
| Framework | Default output directory |
|---|---|
| Vite | dist |
| Create React App | build |
Next.js (output: 'export') | out |
| Astro | dist |
| Vue CLI | dist |
| Nuxt (static) | dist |
- 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
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.