How This Site Is Hosted

How This Site Is Hosted

The site you’re reading is served by the stack it documents. The deployment is also designed to be liftable to Vercel with zero code changes the day you want to flip it.

Today: in-cluster

git.skypalette.ai/skyadmin/sky-palette-infra-tutorial

       │  push to main

Forgejo Actions: `npm ci && npm run build`

       │  produces `out/` (static export, no SSR)

Docker build: nginx:alpine + `out/`

       │  push to git.skypalette.ai/v2/sky-palette-infra-tutorial:<sha>

argocd-image-updater: bumps tag in gitops repo


ArgoCD: syncs new image to the cluster


tutorial.skypalette.ai (via cloudflared → ingress-nginx → cert-manager TLS)

The Next.js config that makes both targets work

// next.config.mjs
import nextra from 'nextra'
 
const withNextra = nextra({
  theme: 'nextra-theme-docs',
  themeConfig: './theme.config.tsx',
})
 
export default withNextra({
  output: 'export',      // produces a static `out/` directory
  images: { unoptimized: true },
  trailingSlash: true,   // matches nginx + Vercel's default routing
})

output: 'export' is the whole trick. The build artifact is a directory of HTML/JS/CSS — no Node runtime needed, no API routes, no Vercel-specific features. Anything that serves static files can serve this site.

The Dockerfile

# Build stage — Node 20, pinned, no telemetry.
FROM node:20-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Serve stage — nginx, just the static output.
FROM nginx:alpine
COPY --from=builder /app/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080

The nginx config sets listen 8080; (so the container runs as non-root) and adds a single try_files directive so /foo falls back to /foo/index.html matching Next’s trailingSlash: true output.

Migrating to Vercel — what changes

git.skypalette.ai/skyadmin/sky-palette-infra-tutorial

       │  push to main (or mirror to GitHub if Vercel needs it)

Vercel: detects Next.js, runs `npm run build`


Vercel CDN: serves the same static export, globally cached


tutorial.skypalette.ai (CNAME flipped to Vercel)

Concrete migration steps the day you decide:

  1. vercel link from this repo’s working tree. Vercel detects Next.js automatically.
  2. Set the project’s “Output Directory” to out if it doesn’t auto-detect from output: 'export'.
  3. vercel --prod → Vercel issues a *.vercel.app URL.
  4. In Cloudflare DNS, change tutorial from CNAMEd to the in-cluster tunnel to CNAMEd to cname.vercel-dns.com. Vercel handles its own TLS.
  5. Delete:
    • The Dockerfile and nginx.conf (Vercel doesn’t use them).
    • The Forgejo Actions workflow (Vercel builds on its own).
    • The gitops base/sky-palette-infra-tutorial/ + overlay + Application.
  6. Keep:
    • Everything else. The Next.js source is identical.

Why static export, not SSR

  • Portability. Static HTML serves identically on any CDN, any nginx, any S3 bucket. SSR ties you to Node-on-host or to Vercel Functions / AWS Lambda.
  • Zero runtime cost. No Node process to OOM, no scaling decisions. An empty nginx pod handles a hundred concurrent readers without breathing hard.
  • Vercel-compatible by default. Vercel happily serves static exports, with the same CDN edge caching it gives full Next.js apps.

The tradeoff: no per-request data, no search indexes built at runtime, no auth-gated pages. For a docs site, that’s a perfect fit.

”Where’s the search built?”

Nextra’s flexsearch index is generated at build time. The whole search corpus lives in a JSON blob shipped with the static export — no search service required, no API endpoint, no API rate limits. The tradeoff is bundle size; for this site it’s a few hundred KB and worth it.