Hugo generates a static site in public/ when you run hugo. This folder contains plain HTML, CSS, and JS that can be hosted anywhere. Below are step-by-step guides for three common platforms.


Netlify

Create netlify.toml in your site root:

toml
[build]
  command = "hugo --gc --minify"
  publish = "public"

[build.environment]
  HUGO_VERSION = "0.139.0"

[context.deploy-preview]
  command = "hugo --gc --minify --buildDrafts --baseURL $DEPLOY_PRIME_URL"

[context.deploy-preview.environment]
  HUGO_VERSION = "0.139.0"

Setup

  1. Push your site to GitHub
  2. Go to app.netlify.com > Add new site > Import an existing project
  3. Select your repo. Netlify auto-detects the netlify.toml
  4. Click Deploy site

The first build takes about 30 seconds. Netlify gives you a URL like https://random-name-12345.netlify.app.

Custom domain

  1. In Netlify: Site configuration > Domain management > Add a domain
  2. In your DNS provider, add a CNAME record pointing your domain to <your-site>.netlify.app
  3. HTTPS is provisioned automatically by Netlify via Let’s Encrypt

Use DNS only (not proxied) if you’re on Cloudflare. Netlify handles its own CDN and certificate.

Auto-deploy

Every push to main triggers a rebuild. Pull requests get deploy previews at unique URLs.

Key settings

SettingWhy
HUGO_VERSION = "0.139.0"Pins the exact Hugo version. Without it, Netlify uses whatever is on their build image
$DEPLOY_PRIME_URLNetlify variable for the correct preview URL, preventing baseURL mismatches
--buildDrafts in deploy-previewLets you preview unpublished posts in PR previews

Updating the theme

bash
git submodule update --remote --merge
git add themes/mono
git commit -m "Update Mono theme" && git push

Note: The Mono theme repo includes its own netlify.toml for building the demo site. If you’re deploying your own site with Mono as a submodule, use the config above instead.


GitHub Pages

Workflow

Create .github/workflows/deploy.yml:

yaml
name: Deploy Hugo site to Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: "0.139.0"
          extended: true

      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v4

      - name: Build
        env:
          HUGO_ENVIRONMENT: production
        run: hugo --gc --minify --baseURL "${{ steps.pages.outputs.base_url }}/"

      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Setup

  1. Push your site to GitHub
  2. Go to Settings > Pages > set Source to GitHub Actions
  3. The workflow runs on every push to main

Your site will be at https://<username>.github.io/<repo>/ (project site) or https://<username>.github.io/ (user site).

baseURL

Set baseURL in hugo.toml to match your GitHub Pages URL:

toml
# Project site (repo named anything)
baseURL = "https://username.github.io/my-site/"

# User site (repo named username.github.io)
baseURL = "https://username.github.io/"

The workflow overrides this dynamically via actions/configure-pages, so even if it doesn’t match, the deploy will work correctly.

Key details

SettingWhy
peaceiris/actions-hugo@v3Fast Hugo install. extended: true gets Hugo Extended
actions/configure-pages@v4Provides the correct baseURL automatically for both project and user sites
submodules: recursiveFetches the Mono theme submodule during checkout
concurrency blockPrevents overlapping deployments

Docker / Self-hosting

For VPS, Docker Compose, Dokploy, Fly.io, Railway, or any container platform.

Dockerfile

Add to your site root:

dockerfile
FROM hugomods/hugo:exts-0.139.0 AS builder
WORKDIR /src
COPY . .
RUN hugo --gc --minify

FROM nginx:1.27-alpine
COPY --from=builder /src/public /usr/share/nginx/html
EXPOSE 80

.dockerignore

.git
.github
public
resources
.hugo_build.lock

Build and run

bash
docker build -t my-site .
docker run --rm -p 8080:80 my-site

Open http://localhost:8080 to verify, then deploy the image to your platform of choice.

Custom baseURL

Override the base URL at build time:

dockerfile
ARG BASE_URL=https://example.com/
RUN hugo --gc --minify --baseURL ${BASE_URL}

Then build with:

bash
docker build --build-arg BASE_URL=https://mysite.com/ -t my-site .

Available Hugo images

The hugomods/hugo images come in variants:

TagIncludes
0.139.0Standard Hugo
exts-0.139.0Hugo Extended + Dart Sass + PostCSS + git

Use the exts variant if your site uses SCSS or PostCSS. Available tags: docker.hugomods.com.


Troubleshooting

Theme not found

  • Verify themes/mono exists: git submodule status
  • Make sure themesDir is NOT set in hugo.toml (Hugo defaults to themes/)
  • For GitHub Actions: submodules: recursive must be in the checkout step

Build fails with SCSS error

  • Hugo Extended is required. On Netlify set HUGO_EXTENDED = "true", on GitHub Actions set extended: true

CSS broken / wrong URLs

  • baseURL mismatch. On Netlify use $DEPLOY_PRIME_URL for previews. On GitHub Pages the configure-pages action handles it automatically

Site loads but pages 404

  • For GitHub Pages project sites, all URLs include the repo name prefix. The dynamic baseURL in the workflow handles this