Static Sites

3 min read

Teploy has two ways to serve a static site. For almost everyone, type: static is the right one — no Dockerfile, no image build, just rsync + Caddy.

Native static deploy (type: static)

Build your site locally (or let Teploy run the build for you), and Teploy rsyncs the output directory straight to the server, symlinks it live, and writes the Caddy route. No Docker involved.

app: mysite
type: static
domain: mysite.com
server: production

source: dist          # local directory of already-built files to ship
build:                 # optional — shell commands run locally before rsync
  - npm run build

Deploy:

teploy deploy

build runs locally by default (set build_remote: true to run it on the server instead), then source is rsynced up. Your site is live at https://mysite.com with auto-provisioned SSL.

See the full field list — SPA fallback, cache headers, release retention, custom Caddy directives — in the teploy.yml reference.

Supported generators

Any static site generator works — point source: at wherever it writes output:

  • Astro (dist/)
  • Next.js static export (out/)
  • Vite / React / Vue (dist/)
  • Hugo (public/)
  • Jekyll (_site/)
  • Eleventy (_site/)
  • Plain HTML/CSS/JS

SPA routing

For single-page applications that handle routing client-side:

spa: true
spa_fallback: /index.html   # default, only needed to override

Caching

cache:
  "/assets/*": "public, max-age=31536000, immutable"

Asset bridging

For zero-downtime static asset serving (old assets stay available for in-flight requests during a deploy transition):

assets:
  path: /srv/assets
  keep_days: 7

Optional: CDN via Cloudflare

For global edge caching, put Cloudflare in front of your server:

  1. Add your domain to Cloudflare
  2. Point to your VPS IP with proxy enabled (orange cloud)
  3. Static assets are cached at 300+ edge locations

See Cloudflare Setup for details.

Docker-based static deploy

If you need more control over the serving layer — a custom base image, non-Caddy tooling baked into the build, or you're already running everything else as containers — you can serve a static site as a regular container deploy instead. This is the older, more manual path; reach for type: static above unless you have a specific reason not to.

Create a Dockerfile for your static site:

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

FROM caddy:alpine
COPY --from=build /app/dist /srv

Then configure teploy.yml as a normal container deploy (no type: static):

app: mysite
domain: mysite.com
server: production
port: 80
teploy deploy

Custom Caddyfile

For advanced routing, add a Caddyfile to your project:

:80 {
    root * /srv
    file_server

    header /assets/* Cache-Control "public, max-age=31536000, immutable"

    handle_errors {
        rewrite * /404.html
        file_server
    }
}

Include it in your Dockerfile:

FROM caddy:alpine
COPY dist /srv
COPY Caddyfile /etc/caddy/Caddyfile

SPA routing with try_files:

:80 {
    root * /srv
    try_files {path} /index.html
    file_server
}