Deployment
Ship the site as a small container image, or to any static host.
Sur cette page
A ZenithDocs build is a folder of static files in dist/: HTML, CSS, a little JavaScript, the search index and the images. Anything that serves files can host it, with no server-side runtime.
npm run buildZenithDocs writes a ready-to-use Dockerfile. Ask for it when creating the site, or add it to an existing one:
New site
npx github:AymericChaverot/zenith-docs create my-docs --dockerExisting site
npx zenith dockerThen build and run the image:
docker build -t my-docs .
docker run --rm -p 8080:8080 my-docsThe site is served at http://localhost:8080.
The Dockerfile has two stages. The first builds the site with Node and is thrown away; only the static files reach the second, which serves them.
| Choice | Why |
|---|---|
nginxinc/nginx-unprivileged:alpine-slim |
About 30 MB in all, with no Node, no package manager and no build tools |
Unprivileged user, port 8080 |
Runs on platforms that forbid root containers, such as OpenShift |
| Files compressed at build time | nginx sends the .gz next to each file, and never compresses on the fly |
immutable cache on /_astro/ |
Those files have a content hash in their name, so they never change |
404.html as the error page |
Unknown URLs get the site’s own not found page |
text/markdown for .md files |
The Markdown version of each page opens instead of downloading |
nosniff and a strict referrer policy |
Security headers that cost nothing on a static site |
| A health check | Orchestrators can tell when the server is ready |
Dependencies are installed before the content is copied, and cached between builds, so editing a page rebuilds the site without reinstalling anything. The package manager follows the lockfile: pnpm when pnpm-lock.yaml exists, npm otherwise.
With base set to /docs, the files have to sit in a docs folder of the server, and the error page moves with them. Two lines of the Dockerfile change, as its own comment says:
error_page 404 /docs/404.html;
COPY --from=build /app/dist /usr/share/nginx/html/docslastUpdated reads the git history, which .dockerignore leaves out of the image to keep builds fast. Remove the .git line from .dockerignore to use it.
services:
docs:
build: .
ports:
- '8080:8080'
restart: unless-stoppeddocker compose up -d --buildThis workflow builds the image on every push to main and publishes it to the GitHub Container Registry, as ghcr.io/<owner>/<repository>:
name: Image
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=sha
- uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxdocker/metadata-action also lowercases the image name, which the registry requires.
Netlify, Vercel, Cloudflare Pages and the others all ask for the same settings:
| Setting | Value |
|---|---|
| Build command | npm run build |
| Output directory | dist |
| Node.js version | 22.18 or later |
Set site to the URL the host gives you, so that canonical links, the sitemap and social images use absolute URLs. A host that serves the site from a sub-path, like GitHub Pages without a custom domain, also needs base.
A repository named my-docs is served at https://<owner>.github.io/my-docs/, so the site needs both options:
export default defineConfig({
title: 'My Docs',
site: 'https://<owner>.github.io',
base: '/my-docs',
});Links written from the root in your pages, like [Install](/installation/), get the base added at build time: there is nothing to change in the content.
This workflow builds the site on every push to main and publishes it. In the repository settings, under Pages, set the source to GitHub Actions.
name: Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The whole history, for the last updated dates.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4This site is deployed the same way, from its repository.