Hosting a Modern Hugo Site on Render.com
Last updated Oct 30, 2025
It’s been a while since I last refreshed my personal site, but for the past few versions I’ve been hosting it for free on Render.com. For static sites, Render is a very simple option, especially if you’re using Hugo.
There is a small problem though. Render’s default build image is quite a few versions behind (v0.124.1 vs v0.152.1 at the time of writing), which means a little extra work is needed to get a modern site hosted.
There are a few instances of this problem catching out others, and I can’t find any official line on it from Render.
A workaround has been covered by Mike Zornek, using a shell script to download Hugo directly. As he notes though, this means downloading Hugo on every release, which feels like a pretty inefficient way to deploy a static site.
I’ve gone a slightly different route, using GitLab’s 400 free compute minutes per month to build the site through its CI pipeline. This lets me pick the exact Hugo version I want, build locally within CI, and then push the output to a separate branch that Render pulls from to deploy.
Here’s the GitLab pipeline that builds the site and publishes it to a branch that Render can deploy:
# .gitlab-ci.yml
stages:
- build
- publish
image: hugomods/hugo:0.151.2
variables:
HUGO_ENV: production
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- resources/_gen/
- .hugo_cache/
build:
stage: build
script:
- hugo --gc --minify
artifacts:
paths:
- public
expire_in: 1 hour
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
publish:
stage: publish
needs: ["build"]
image: alpine:3.20
script:
- set -e
- apk add --no-cache git rsync
- git config user.name "gitlab-ci"
- git config user.email "ci@example.com"
- git config --global --add safe.directory "$CI_PROJECT_DIR"
- export GIT_ASKPASS=/bin/true
# Use CI Job Token username if available; fallback to literal for older runners
- export PUSH_USER="${CI_JOB_TOKEN_USER:-gitlab-ci-token}"
- export DEPLOY_REMOTE="https://${PUSH_USER}:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
# Single authenticated remote for all ops
- git remote remove deploy 2>/dev/null || true
- git remote add deploy "$DEPLOY_REMOTE"
- git fetch --prune deploy
# Create render-dist remotely if missing
- |
if ! git ls-remote --exit-code --heads deploy render-dist >/dev/null 2>&1; then
git checkout --orphan render-dist
rm -rf *
git commit --allow-empty -m "feat(deploy): initialize render-dist"
git push deploy HEAD:render-dist
git switch "${CI_COMMIT_BRANCH:-main}"
fi
# Attach worktree to render-dist
- git worktree prune || true
- git worktree add /tmp/render-dist deploy/render-dist || git worktree add /tmp/render-dist render-dist
# Sync build output WITHOUT nuking the .git file in the worktree
- test -d public
- rsync -a --delete --exclude='.git' public/ /tmp/render-dist/
# Copy render.yaml for Render.com to see
- rsync -a --delete render.yaml /tmp/render-dist/
# Safety check: ensure worktree still a repo
- test -f /tmp/render-dist/.git
# Commit & push only if there are changes
- cd /tmp/render-dist
- git add -A
- git diff --quiet --cached || git commit -m "Deploy ${CI_COMMIT_SHORT_SHA} at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
- git push deploy HEAD:render-dist
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
At a high level:
- The
buildjob runs Hugo using a recent Hugo image (hugomods/hugo:0.151.2), not Render’s older one. This is where the actual site gets built. - The output from that build ends up in
public/, and we keep it as an artifact for the next stage. - The
publishjob then takes that built site and commits just the generated HTML, CSS, and other static files to a dedicated branch calledrender-dist. - Render is configured to deploy from that
render-distbranch, so Render never has to run Hugo itself. It just serves the already-built static files.
There are a few different approaches to this problem, but I went with this solution for a few key reasons:
- I control the Hugo version instead of waiting for Render to update theirs, but I also don’t have to wait for Hugo to be installed on every build.
- I only pay in GitLab CI minutes, which are free up to 400 minutes a month. I also have experience with GitLab pipelines from my day job, which made setup straightforward.
- This leaves Render to simply deploy static files, which is what it’s good at.
And for completeness, here is my render.yaml, the configuration for Render that lives in the root of the project:
services:
- type: web
name: domdo.es
env: static
branch: render-dist # deploy the prebuilt branch
staticPublishPath: ./ # files live at repo root on render-dist
autoDeployTrigger: commit
Is this a perfect solution? Definitely not. It would be nice if Render provided a build image with an up-to-date version of Hugo. Then again, it is doing a lot for me for free.
