Skip to Content
PyoDocs

Static sites

Deploy plain HTML or a built frontend from Git.

A static site is a folder of HTML, CSS, JavaScript and images. Pyo deploys it like any other service from Git. With no Dockerfile, Pyo builds it with Railpack, which serves the files with Caddy. The build log says Static site for plain files, or the framework's name for a built frontend.

Before you deploy

Keep the default port, 8080. Pyo sets PORT to the service's first port, and Railpack's Caddy listens on PORT.

Health check

When the build finds a static site, Pyo checks that / answers before the new version takes traffic. A redirect counts too. The service's Health check shows Automatic (static site).

To use another check, change it on the service's Resources tab. Pyo then keeps yours. Choose HTTP path for another path, TCP to only wait for the port, or None to not wait at all.

Plain HTML

Push a repository with an index.html at its root, and that's it. Pyo serves the whole repository, except .git and .env files.

To serve another folder, Railpack looks in this order:

  1. A RAILPACK_STATIC_FILE_ROOT build argument, like site.
  2. A file named Staticfile at the root, with root: site.
  3. A public folder.
  4. An index.html at the root.

A request for /about finds about, about.html or about/index.html.

A built frontend

Pyo runs your build script, then serves only its output. Railpack finds the output folder for:

  • Vite, including React, Vue and Svelte projects built with Vite
  • Astro, when it has no server output or adapter
  • Next.js with output: 'export'
  • Create React App
  • Angular

It reads a custom outDir from your Vite or Astro config. For anything else, like SvelteKit with adapter-static, add a build argument with the output folder:

RAILPACK_SPA_OUTPUT_DIR=build

That also tells Railpack to serve the folder as a static site, whatever the framework. Variables your build reads, like VITE_API_URL, go in build arguments too, since the build runs before the service starts.

Add build arguments in Deploy new, under Advanced: Dockerfile, build context and arguments, or later in the service's Settings, under Deployment source. They're used on the next deploy. To change the build command, add RAILPACK_BUILD_CMD, like npm run build:prod.

Static site generators

Many generators' start script runs their development or preview server, like eleventy --serve, astro dev, vite preview or hugo server. That's a full Node.js process, often 100 MiB of memory or more, to serve files. When Pyo finds one, it runs your build script and serves the output with Caddy instead, in around 15 MiB:

GeneratorOutput folder
Eleventy_site, or the output in your Eleventy config or --output in the build script
Astro, with no server output or adapterdist, or outDir in your Astro config
Hugo, built through npm (like hugo-bin)public, or -d in the build script
Vitedist, or outDir in your Vite config

Eleventy and Hugo are served this way with no start script too. The build log says which folder it serves, and the site gets the automatic health check.

A generated site has a page for each path, so a path that matches nothing gets a 404 instead of the home page. A Staticfile of your own changes that.

To run your start script instead, add the build argument:

RAILPACK_NO_SPA=1

Pyo also leaves it alone when you set RAILPACK_SPA_OUTPUT_DIR, RAILPACK_START_CMD or RAILPACK_STATIC_FILE_ROOT, or have a Procfile or a start command in railpack.json.

Jekyll, and Hugo without a package.json, need a Dockerfile, since Railpack can't build them on its own.

Routing in single page apps

A single page app handles its own paths, like /settings, so every path that isn't a file should get index.html.

  • A built frontend: this is on by default, except for a static site generator's output that Pyo serves.
  • Plain HTML: turn it on with a Staticfile at the root:
root: .
index_fallback: true

Set index_fallback: false to turn it off for a built frontend. Without the fallback, a path that matches nothing gets a 404, or your 404.html for plain HTML sites.

A site in a subfolder

If the site lives in a folder of a bigger repository, set the Build context to that folder, like apps/web. Pyo builds and serves it from there.

Your own Dockerfile

If you add a Dockerfile, Pyo uses it instead of Railpack. This one builds a Vite site and serves it with nginx on port 8080, with every unknown path going to index.html:

Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
nginx.conf
server {
    listen 8080;
    root /usr/share/nginx/html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Set the health check to HTTP path /, since Pyo only picks one for Railpack's static sites. Pyo passes your build arguments to the Dockerfile as ARG values.

The Nginx template

Templates has Nginx. It serves nginx's welcome page on port 80, and is a starting point for your own image: build one FROM nginx with your files in /usr/share/nginx/html, then deploy it from Git or a container image.

Good to know

  • Served from your servers: the files come from your servers, not a global CDN. For visitors far from them, put a CDN like Cloudflare in front.
  • Always on: the site runs as a small service that's always on, with its own replicas and resources. See Scaling.
  • Everything else works the same: domains and HTTPS, the firewall, the WAF and deploy on every push.
  • Security headers: for plain HTML sites, Railpack's Caddy adds a few headers, including a Content-Security-Policy. If it blocks something your site needs, use your own Dockerfile.

On this page

Edit page