# Host a static site (Astro, Hugo, Next.js) with its own subdomain

> Build an Astro, Hugo, Vite or Next.js static export into a non-root nginx image on port 8080 and host it on blitz.cloud with HTTPS on your own subdomain.

Web page: https://blitz.cloud/guides/host-static-site/
Updated: 2026-09-26

To host a static site on blitz.cloud, you build it into plain files, copy them into the unprivileged nginx image, push that image to Docker Hub, and deploy it. The site gets its own subdomain like `blog.yourname.blitz.cloud` with HTTPS. This works for any generator that outputs a folder of HTML: Astro, Hugo, Eleventy, a Vite app, or Next.js with `output: "export"`.

Honest comparison first. Netlify, Cloudflare Pages and GitHub Pages build static sites from a Git repository and serve them from a global CDN, and for a plain website that is less work than this guide. blitz.cloud builds from a repository too once you connect GitHub, but it serves from one location, not a CDN. It is worth it if you want the site next to your other apps under one address, run by a German company on servers in the EU.

## The Dockerfile

This is the [static-site example](https://github.com/blitzworks-io/blitz-cloud-examples/tree/main/static-site), an Astro site. Two stages: Node builds the site, nginx serves it.

```dockerfile
# Build stage: turn the Astro project into plain files in dist/.
FROM node:22.23.2-alpine3.24 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Serve stage: nginx without root, listening on 8080.
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
# blitz.cloud runs every app as uid 1000. nginx-unprivileged keeps its pid
# file and temp folders in /tmp, which any uid can write to.
USER 1000
EXPOSE 8080
```

The regular `nginx` image listens on port 80 and starts as root. That works on blitz.cloud too, inside the app's own sandbox. We still use `nginxinc/nginx-unprivileged`, the official variant without root, because it doesn't ask for rights it never uses.

## The nginx config

```nginx
server {
    # The unprivileged nginx image listens on 8080 instead of 80, so it runs
    # without root.
    listen 8080;
    server_name _;
    root /usr/share/nginx/html;

    # Behind blitz.cloud's HTTPS proxy nginx only sees plain HTTP on 8080.
    # Relative redirects keep /about -> /about/ on the address the visitor used.
    absolute_redirect off;

    location / {
        try_files $uri $uri/ $uri.html =404;
    }

    # Astro puts fingerprinted assets in /_astro/, safe to cache for a year.
    location /_astro/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    error_page 404 /404.html;

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}
```

`absolute_redirect off` is the line people miss. Without it, a visit to `/about` gets redirected to `http://blog.yourname.blitz.cloud:8080/about/`, because nginx builds the redirect from what it sees, and behind the HTTPS proxy it sees plain HTTP on port 8080. We hit this while testing the example.

## Other generators

Only the build stage changes. Point the `COPY --from=build` line at the generator's output folder.

| Generator | Build command | Output folder |
| --- | --- | --- |
| Astro | `npm run build` | `dist` |
| Vite (React, Vue, Svelte) | `npm run build` | `dist` |
| Next.js with `output: "export"` | `npm run build` | `out` |
| Hugo | `hugo --minify` | `public` |

For Next.js, a static export only covers pages that don't need a server. If your app uses server components that fetch per request, API routes or middleware, use the standalone setup from [Move an app from Vercel to an EU host](https://blitz.cloud/guides/migrate-from-vercel-to-eu/) instead.

For a single-page app with client-side routing, replace `=404` in `try_files` with `/index.html` so deep links load the app.

## Build, test and push

```bash
docker build --platform linux/amd64 -t yourname/blitz-example-static-site:1.0.0 -t yourname/blitz-example-static-site:latest .
docker run --rm --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges \
  -p 8080:8080 yourname/blitz-example-static-site:1.0.0
```

In our run, `/` and `/about/` returned `200`, `/about` returned a `301` to `/about/`, and a missing page returned a real `404`. Then push:

```bash
docker push --all-tags yourname/blitz-example-static-site
```

## Put it online with your subdomain

1. In the [dashboard](https://dashboard.blitz.cloud/), click Host something new and choose "An app from Docker Hub".
2. Search for your image, pick the version, click Continue.
3. Give it an address. Type `blog` and the site is at `https://blog.yourname.blitz.cloud`. You can also put one app on your main address itself, `https://yourname.blitz.cloud`, which suits a personal homepage.
4. Click Put it online.

You can also skip the push. Keep the Dockerfile and `nginx.conf` in your GitHub repository, connect your GitHub account, choose "My own code" and pick the repository. Every push to the branch then rebuilds the site and puts it online.

The certificate covers your main address and every subdomain under it, and renews on its own. See [Addresses and HTTPS](https://blitz.cloud/docs/addresses-and-https/).

## Can I use my own domain?

Yes, on Pro. Connecting a domain you own, like `example.com`, is part of the Pro plan at 7 EUR per month. On the free plan the site lives under `yourname.blitz.cloud`.
