> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slipway.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Static sites

> Build a site once and serve it from the CDN, with no container to run.

slipway runs your build command, uploads the directory it produces, and serves those files
from the edge. A static site has no container, so it has no compute allocation and nothing
to pay for by the minute. You are metered on bandwidth only.

```yaml slipway.yaml theme={"system"}
version: 1

services:
  - name: site
    type: static
    runtime: node
    runtimeVersion: "22"
    buildCommand: npm run build
    publish: dist
```

Push to the tracked branch and it rebuilds. The new build goes live the moment its upload
finishes.

## publish

**Required.** The directory your build writes, relative to `rootDir`.

```yaml slipway.yaml theme={"system"}
    rootDir: web
    publish: dist          # uploads web/dist
```

Common values:

| Tool | `publish` |
| - | - |
| Vite, Astro | `dist` |
| Create React App, SvelteKit static | `build` |
| Next.js export | `out` |
| Hugo, Gatsby | `public` |
| Jekyll, Eleventy | `_site` |

Getting this wrong is the most common way a static site fails: the deploy goes green and
the URL returns 404. The build log reports how many files were uploaded, and the service's
**Files** view lists exactly what is there.

## The build

`rootDir` is where the build runs, defaulting to the repository root. `buildCommand` is
what it runs, and needs `runtime` to say what to run it in.

```yaml slipway.yaml theme={"system"}
    rootDir: .
    runtime: node
    runtimeVersion: "22"
    buildCommand: npm run build
```

Dependencies are installed before `buildCommand` runs, from whichever lockfile the project
commits: `npm ci`, `pnpm install` or `yarn install` for Node, `pip install` for Python,
`bundle install` for Ruby. The build command is only the build. Writing `npm ci && npm run
build` still works, it just installs twice.

All four settings can be changed later from the site's **Settings** tab, under **Build**.
Saving rebuilds and republishes the site. A site that deploys from a committed
`slipway.yaml` shows the file's values there, and is changed in the file.

Omit `buildCommand` when the files are already committed and need no build:

```yaml slipway.yaml theme={"system"}
  - name: docs
    type: static
    rootDir: site
    publish: .
```

> [!NOTE]
>
> `dockerfilePath`, `dockerContext`, `dockerTarget` and `image` are all rejected on a static
> service. No image is built and none is pulled.

## Uploading a build instead

A static site is the one thing slipway serves with no repository, because the artifact is
the whole input. In **New → Static site**, switch the source to **Upload a build** and drop
a `.zip` of your build output.

Re-upload at any time to publish a new version. Each upload replaces the last, and
deploying publishes it the same way a repo build would, with the same events, rollback and
file views.

A zip made by compressing a directory wraps everything in one top-level folder. slipway
strips a single wrapper automatically, so both of these serve the same site:

```
dist/index.html          index.html
dist/assets/app.js       assets/app.js
```

Two or more top-level entries are treated as the site itself rather than a wrapper.
`__MACOSX/` and `.DS_Store` are skipped, and an archive containing `..` in a path is
rejected rather than relocated.

An uploaded site has no build command, no build-time environment variables and no branch to
watch, so nothing auto-redeploys. Everything else on this page works identically.

## Environment variables

A static site has no runtime, so `env` applies to the **build** only.

```yaml slipway.yaml theme={"system"}
    env:
      VITE_API_URL: ${service.api.public_url}
```

That is the common case: a frontend baking in the URL of an API it deploys alongside. The
reference resolves at deploy time, before the build runs.

> [!WARNING]
>
> Anything referenced here ends up inside the built files, which are public by definition.
> Bundlers inline environment variables at build time, so a `${secret.*}` reference here is
> readable by anyone who views source. Do not put secrets in a static site's `env`.

## Single-page apps

Set `notFound` to your entry document so client-side routes resolve.

```yaml slipway.yaml theme={"system"}
    publish: dist
    notFound: /index.html
```

Requests that match no file are served `index.html`. Requests that look like assets, a
dotted filename that is not `.html`, still return a real 404, so a missing `/app.js` does
not come back as HTML and surface as a JavaScript syntax error.

For a multi-page site, point `notFound` at a real error page instead:

```yaml slipway.yaml theme={"system"}
    notFound: /404.html
```

A `404.html` in the publish directory is used automatically even without `notFound`, and
the nearest one wins, so `docs/404.html` answers misses under `/docs/`.

## Redirects and headers

```yaml slipway.yaml theme={"system"}
    redirects:
      - { from: /old-blog/*, to: /blog/:splat, status: 301 }
      - { from: /app/*, to: /index.html, status: 200 }
    headers:
      - for: /assets/*
        values:
          Cache-Control: "public, max-age=31536000, immutable"
```

A real file beats a `200` rewrite; set `force: true` to override that. A `3xx` rule beats a
real file, so you can retire a live URL without deleting the page behind it.

An empty header value removes the header.

slipway also reads `_redirects` and `_headers` files from the publish directory, in the
Netlify and Cloudflare Pages format, so a site moving from either platform works unchanged.
A syntax error there does not fail the deploy: the bad line is skipped and named in the
build log. The files themselves are never served.

The same rules can be edited in the console without a rebuild. See
[Redirects and headers](/networking/static-routing) for how the three sources combine and
which wins.

## Caching

slipway sets caching for you:

* Hashed assets, meaning anything under `/assets/`, `/_app/immutable/`, `/_next/static/`,
  or with a content hash in the filename, get
  `Cache-Control: public, max-age=31536000, immutable`.
* Everything else revalidates, so a deploy is visible immediately.

Override either with a `headers` rule.

You never need to purge a cache. Each deployment's files are served from their own address
at the edge, so a deploy or rollback moves to fresh cache entries rather than invalidating
old ones.

## Deploys and rollbacks

A deploy is atomic. The previous build serves until the new one is completely uploaded,
then the switch happens between requests.

Rolling back rebuilds nothing, because the previous build's files are still there, so it is
a single pointer change. Older builds are kept for 30 days and at least the last 10, which
is how far back you can roll.

A switch takes a few seconds to reach every location and up to about a minute worldwide.
Nobody ever sees a mixture of two builds.

## Custom domains

Add the domain on the project's **Settings → Domains**, then point one DNS record at
slipway:

```
CNAME   www   cname.<your-apps-domain>
```

The certificate is issued and renewed automatically once that record resolves.

For a root domain, DNS does not allow a CNAME at the apex. If your provider supports ALIAS,
ANAME or CNAME flattening, use the same target with that record type. If it does not,
slipway shows A records to use instead, or you can redirect the apex to `www`.

See [Custom domains](/networking/domains).

## Traffic

A static site is served from the CDN and the cluster is never in the request path, so
there are no pod counters and nothing for the Metrics tab to sample. Its traffic is
measured at the edge instead, and shows up in two places.

The site's own page carries **Egress** and **Requests** for the last 24 hours, hour by
hour, along with how those requests were answered. An hour that is tall on requests and
flat on egress is a site answering `304 Not Modified`, which means caching is working.

The project overview adds a **CDN** panel whenever the project runs any static services,
with one bar per hour and a colour per site. It sits beside CPU, Memory and Requests
rather than joining them: those cover the project's containers at five-minute resolution,
and edge traffic is only reported by the hour. Drawing the two on one axis would make a
site serving the same rate as a container look twelve times busier.

Edge measurements arrive a couple of minutes behind, so the most recent bar keeps filling
in for a short while after a burst.

## What a static site does not have

No shell, no scaling, no disks, no compute allocation and no sleep, because there is no
container for any of those to act on. The console hides those tabs rather than locking
them.

Siblings cannot reach a static site over the private network. There is no `http://site`
inside the project because no process is listening, and `${service.site.host}` fails with a
message pointing at `${service.site.public_url}`.

## Pricing

Static sites are not billed for compute, and their builds are free. You pay for egress
beyond your plan's allowance. See [Plans](/plans).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.