Skip to content

Static Site Deployment (Hugo / 11ty)

Architecture

User → Nginx (host) → Static files on disk (/var/www/html)

Hugo

Project Setup

# Install Hugo
apt install -y hugo
# OR: download from https://github.com/gohugoio/hugo/releases

# Create new site
hugo new site my-site --format yaml
cd my-site

# Add theme (git submodule)
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke

# Create content
hugo new posts/first-post.md

Build

# Build to ./public
hugo

# Build with base URL
hugo --baseURL "https://example.com"

# Development server
hugo server -D

# Minify output
hugo --minify

Dockerfile

Dockerfile
FROM hugomods/hugo:latest AS builder
WORKDIR /src
COPY . .
RUN hugo --minify --baseURL "https://example.com"

FROM nginx:alpine
COPY --from=builder /src/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
docker-compose.yml
services:
  app:
    build: .
    ports:
      - "127.0.0.1:8080:80"

11ty (Eleventy)

Project Setup

npm init -y
npm install @11ty/eleventy --save-dev
package.json
{
  "scripts": {
    "build": "eleventy",
    "serve": "eleventy --serve"
  }
}

Build

npm run build
# Output in ./_site

Dockerfile

Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/_site /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

Nginx Config

nginx.conf
server {
    listen 80;
    server_name example.com;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip
    gzip on;
    gzip_types text/html text/css text/javascript application/javascript image/svg+xml;
    gzip_min_length 256;

    # Cache static assets
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|webp|svg|woff2?)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # HTML (short cache for updates)
    location ~* \.html$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }

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

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

Deployment

# Option 1: Direct copy to server
rsync -avz --delete ./public/ user@server:/var/www/html/

# Option 2: Docker
docker compose build
docker compose up -d

# Option 3: Cloudflare Pages (recommended)
npx wrangler pages deploy ./public --project-name=my-site

GitHub Actions

Hugo

name: Deploy Hugo

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: "latest"

      - name: Build
        run: hugo --minify --baseURL "https://example.com"

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          source: "public/*"
          target: "/var/www/html"
          strip_components: 1

11ty

name: Deploy 11ty

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "18"

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          source: "_site/*"
          target: "/var/www/html"
          strip_components: 1

Verification

  • Site builds without errors
  • All pages accessible
  • Images and assets load
  • Gzip compression enabled
  • Cache headers set
  • Security headers present
  • CI/CD deploy works