Astro and Hugo Sites on an nginx Container Versus the Pages Hosts: When a Container Wins
A static site built with Astro, Hugo or any other generator has a natural home, and it is not a container. GitHub Pages, Cloudflare Pages and Netlify serve static files for free, from a CDN, without ever sleeping. So why would anyone put one in a container? Because a handful of situations break the Pages model: private repositories on a free GitHub account, a site that needs a password, custom headers and redirects without platform-specific config, or a front end that must live on the same host as its API. This post lays out both sides with the actual limits, and gives a Dockerfile for the cases where the container wins.
What the Pages hosts give you, precisely
| Host | Free tier | The limit that matters |
|---|---|---|
| GitHub Pages | Unlimited sites, custom domain with HTTPS | Public repositories only on a free account; soft limit of 100 GB bandwidth a month; no server logic. |
| Cloudflare Pages | Unlimited bandwidth and requests, 500 builds a month, custom domains | Functions run on the Workers free plan: 100,000 requests a day and 10 ms of CPU per request. |
| Netlify | 300 credits a month (accounts created since September 2025), custom domains, deploy previews | Bandwidth costs 20 credits per GB, so about 15 GB a month if the credits all go to traffic. |
None of them sleeps, all of them are on a CDN, and all of them give you a custom domain for free. For a public site whose only job is to be fast and always there, one of these three is the answer, and the rest of this post is about the exceptions.
When a container wins
- The repository is private and the account is free. GitHub Pages needs a public repository on a free plan. A container host builds from a private repository without asking.
- The site needs a password. Internal documentation, a client preview, a staging copy. nginx does HTTP basic auth in three lines; Pages hosts either cannot or charge for it.
- Headers, redirects and rewrites beyond what a platform's
_headersor_redirectsfile allows: a real nginx config, versioned with the site. - Same host as the API. Serving
/from nginx and proxying/apito a backend removes CORS from your life. On SnapDeploy the front end and the API can be two of the four free containers, or one container with nginx proxying to a local process. - No build-minute meter. Container builds count as deploys (10 a day on the free tier), not minutes.
And the honest cost: a free container sleeps after 15 minutes without traffic and takes about a minute to wake. For a public marketing site that is disqualifying; for internal docs, a preview or a demo it is fine, and Always-On ($12/month) removes it if the site graduates.
The Dockerfile: build the site, serve it with nginx
Astro, or any Node-based generator (the same shape works for Eleventy, VitePress and Docusaurus):
FROM node:22-alpine AS build
WORKDIR /site
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /site/dist /usr/share/nginx/html
EXPOSE 80Hugo is a single binary, so the build stage is smaller; the community hugomods/hugo images ship it with the extended features, or install the release tarball into an Alpine stage. Either way the output folder (public/ for Hugo, dist/ for Astro) is what gets copied into the nginx image.
# nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location = /health { return 200 "ok"; }
location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location /api/ { proxy_pass http://127.0.0.1:8000/; } # remove if the API is elsewhere
location / {
try_files $uri $uri/ $uri.html /404.html;
add_header Cache-Control "public, max-age=300";
}
}Add auth_basic "Private"; auth_basic_user_file /etc/nginx/.htpasswd; inside the root location to password-protect the whole site; generate the file with htpasswd and copy it in the Dockerfile.
Without a Dockerfile, SnapDeploy treats a repository whose root contains plain HTML as a static site and generates an nginx image with a single-page fallback (try_files … /index.html) on port 80. That works for a hand-written site or for a generator whose output you commit to the repository; for Astro or Hugo you want the build stage above, so keep the Dockerfile.
What it costs to run
nginx serving a static site idles at a few megabytes of memory and is limited by network, not by CPU, so the smallest container is more than enough. Image size is the nginx base plus your files: about 15 MB plus the site. Builds are the slow part on a Node generator (dependency install and the generator itself), a few minutes per deploy; Hugo builds in seconds.
Deploying
- Add the Dockerfile and
nginx.conf; commit a.dockerignorewithnode_modulesand the output folder. - Push. Containers → Deploy from GitHub → pick the repository. Port 80.
- Deploy, open the container URL, then
/health. - For a preview per branch, deploy the branch to a second container; the free tier has four.
Decision in one paragraph
Public site, no auth, no special routing: GitHub Pages if the repository is public, Cloudflare Pages if bandwidth might matter, Netlify if you value its build and preview tooling. Private repository, password, custom nginx rules, or a front end that belongs next to its API: a container, accepting the sleep on the free tier or paying $12/month to remove it. Pick by the shape of the site, and keep the build in a Dockerfile either way so the choice is reversible.
Cache headers do real work here
Container URLs on SnapDeploy sit behind Cloudflare, which caches static assets at the edge and honours the Cache-Control headers nginx sends. With the one-year immutable rule on hashed assets above, repeat visitors and most crawlers are served from the edge without reaching the container at all, which both speeds up the site and keeps the container from waking for asset requests. The short five-minute rule on HTML pages keeps deploys visible quickly while still absorbing bursts.
Numbers used: GitHub Pages, Cloudflare Pages and Netlify limits from their documentation and pricing pages as of September 2026; SnapDeploy free tier from the free tier docs (4 containers, 512 MB, 0.25 vCPU, 100 container-hours a month, 10 deploys a day, no card).