# Static sites

A folder of HTML, CSS and JavaScript, a Vite project, or a static site generator deploys with no server, no `Dockerfile` and no configuration. Nimbus Land builds the site when there is something to build, publishes the files, and serves them from its shared static gateway. A static site runs no instances, and each new release goes live in seconds once it is published.

## What is detected as a static site

A static site is detected only when nothing else claims the source. Highest first:

1. **A root `Dockerfile`** always makes a container app. See [Dockerfile](dockerfile.md).
2. **Node server frameworks** (Next.js, Nuxt, Express, Nest and similar) are Node apps, even when they bundle a client with Vite. See [Node.js](nodejs.md).
3. **A root `vite.config.js`, `.ts`, `.mjs` or `.mts`**: **Static site (Vite)**. Dependencies are installed, the `build` script runs, and `dist/` is published.
4. **A `package.json` that builds a site rather than runs a server**: **Static site (npm build)**. It has no server dependency and no server-style `start` script (either none, or a development watcher such as `vite`, `nodemon` or `astro dev`), and one of:
   - a root `index.html` and no runtime `dependencies`;
   - a `build` script that runs a static generator (`astro build`, `eleventy`, `gatsby build`, `vitepress build`, `vuepress build`, `hugo`, `jekyll build`, or `vite build`);
   - a committed `dist/`, `public/` or `build/` folder holding `index.html`.
5. **A root `index.html` with no `package.json`**: **Static site**. Nothing is built.

To see what Nimbus Land would choose, run `nimbus detect` in your project:

```sh
$ nimbus detect
detected: Static site (Vite)
```

## Choosing static or container yourself

Detection is a guess, so every deploy can override it:

- **Deploy wizard.** **Deploy as** offers **Auto-detect**, **Static site** and **Container**. It is shown expanded when detection is less sure (the npm build case above). Choosing **Static site** adds an optional **Publish directory** field.
- **CLI.** `nimbus deploy --static`, optionally with `--publish-dir <dir>`. `--builder container` forces a container build even for a Vite project. In `.nimbus.yml`, use `builder: static` and `publish_dir:` under `build:`.
- **Build API.** `POST /api/v1/builds` accepts `"builder": "static"` or `"container"`, and an optional `static_publish_dir`.

**Static site** outranks every detection, a root `Dockerfile` included. If the root `package.json` has a `build` script, dependencies are installed and the script runs; otherwise the files are published as they are.

The publish directory must be a relative path with no `..` and no leading `/`, at most 128 characters, and must contain an `index.html`. Leave it blank to use the default order described below.

Your choice is saved with the build, so rebuilds and pushes to GitHub keep it.

**Container** is useful when a repository has an incidental `index.html` (a demo page next to a real server).

## How a site is built

Sites with a `build` script are built with Node 22. Dependencies are installed with the package manager your lockfile names:

| Committed lockfile | Install command |
| --- | --- |
| `pnpm-lock.yaml` | `pnpm install --frozen-lockfile` |
| `yarn.lock` (Yarn 2 and later) | `yarn install --immutable` |
| `yarn.lock` (Yarn 1) | `yarn install --frozen-lockfile` |
| `package-lock.json` or `npm-shrinkwrap.json` | `npm ci` |
| none | `npm install` |

The `build` script then runs with the same package manager. A `packageManager` field in `package.json` pins the Yarn or pnpm version. `engines.node` is not read, and `node_modules` is not cached between builds, so commit a lockfile to make installs reproducible.

## What gets published

One directory is published, the first that applies:

1. the publish directory you set;
2. the output directory of a known generator in your `build` script;
3. `dist/` for Vite; the first of `dist/`, `build/`, `out/`, `public/` for npm builds; `public/` for plain sites;
4. the repository root.

| `build` script runs | Published from |
| --- | --- |
| `astro build`, `vite build` | `dist/` |
| `eleventy`, `jekyll build` | `_site/` |
| `gatsby build`, `hugo` | `public/` |
| `vitepress build` | `.vitepress/dist/` |
| `vuepress build` | `.vuepress/dist/` |

A generator configured to write somewhere else needs a publish directory. For Vite, a custom `build.outDir` is not read.

Everything in the published directory is public, except:

- **Files and folders whose names start with `.`** (`.env`, `.npmrc`, `.git/`) are never published, at any depth, and the build log lists what it left out. `node_modules/` is never published either.
- **`.well-known/`** at the top of the site is the exception, so `/.well-known/security.txt` keeps working.

**Publishing the repository root.** If nothing else applies, the whole repository is published, and every file in it (`README.md`, `package.json`, `src/`) is public. When the root looks like source code, the build log warns:

```text
Nimbus: publishing the whole repository (no public/, build output or publish directory) — source files are public. Add public/, a build script, or set a publish directory to publish only the site.
```

Move the site into `public/`, add a `build` script that writes to `dist/`, `build/` or `out/`, or set a publish directory.

**The entry page.** The published directory must have an `index.html` (or `index.htm`) at its top level. Without one, the build fails before anything goes live:

```text
Nimbus: public/ has no index.html, so the site would answer 404. Check the build script's output directory or set the publish directory.
```

## Caching

HTML is revalidated on every visit, so a new release is visible as soon as it is live. A Vite site's hashed files under `assets/` (such as `assets/index-a1b2c3.js`) are cached for a year, since their names change with their content. Other files are cached for an hour, so give a changed file a new name if visitors must see it at once.

## Single-page apps and 404s

| Request | Static site | Static site (npm build) | Static site (Vite) |
| --- | --- | --- | --- |
| `/about` (no such file) | 404 | 404 | `index.html` |
| `/assets/app-a1b2c3.js` (missing) | 404 | 404 | 404 |
| `/docs/` | `docs/index.html` | `docs/index.html` | `docs/index.html` |

A Vite project is treated as a single-page app: links without a file extension serve `index.html` so your client-side router can take over. Missing files that look like assets still return 404. Other static sites return real 404s, so use directory-style pages (`about/index.html`) for clean URLs.

## Releases and rollback

Every deploy creates a release, listed on the app's **Deployments** tab. Releases are published to their own location and never changed, so switching to a new release is all-or-nothing: visitors never see a half-updated site. Rolling back switches to an earlier release without rebuilding, in seconds.

The five most recent releases of each environment can be rolled back to. Older releases stay in the history but their files are removed.

The **Overview** reports a site as healthy while its current release is published. If its files are missing, the Overview says the site is no longer published and offers a redeploy.

## Domains and analytics

The `nimbusapps.ca` address, [custom domains](custom-domains.md) and deploying on every push to GitHub work exactly as for other apps.

When **Measure visitors in the browser** is on in the app's **Analytics** tab, the measurement script is added to every HTML page as it is served, without republishing. See [Traffic and analytics](traffic-and-analytics.md).

## Build-time variables are public

A static site has no server. Variables you set on the app are passed to the build, and Vite inlines every `VITE_*` variable into the JavaScript it produces, where every visitor can read it. Never put an API key, database URL or other secret in a static site's variables. Anything secret belongs behind an app with a server (see [Secret environment variables](secret-env-vars.md)).

Changing a variable needs a new build: choose **Rebuild** or push a commit.

## Limits

- No `_redirects` or `_headers` files: custom redirects and headers are not supported. Single-page fallback is the only routing rule, and it applies to Vite sites.
- Static sites have no instances, scaling, shell, one-off commands or volumes.
- An app deployed as a container stays a container on rebuild. To serve it as a static site, deploy it under a new app name.

## Troubleshooting

**A variable change did nothing.** Variables are built into the site. Rebuild, or push a commit.

**`/about` returns 404 but `/about.html` works.** Only Vite sites get single-page fallback. Use `about/index.html`, or build the site with Vite if it uses client-side routing.

**Vite sites are published from dist/ - check that the build writes there.** Your Vite config writes elsewhere, or the build script doesn't run `vite build`. Keep the default `dist/` or set a publish directory.

**`… has no index.html, so the site would answer 404`.** The build published the wrong folder, usually because your `build` script writes somewhere the default order doesn't look. Set the publish directory to where the script writes.

**Detected as a Node app, but it's a static site.** If the app keeps restarting because its `start` script is a development watcher, the deploy wizard warns before you deploy. Choose **Deploy as** → **Static site**, with a publish directory if your output isn't in `dist/`, `build/`, `out/`, `public/` or the root.
