Eleventy on GitHub pages in 2026
So, this blog uses 11ty to build a static site. Which is great and means I don't have to do much in terms of server admin for the blog anymore. I just use GitHub actions to build it and GitHub pages to deploy it.
I wrote a previous blog post on deploying Eleventy with github pages, but things have changed a lot since 2020. Most notably the number of supply chain attacks has gone up astronomically and we've learned a few things you should defintiely do to protect your deployment pipelines and local development from credential stealers. Most of these are not Eleventy specific and are just useful for node dependency and environment hygiene these days.
This is basically an eleventy build with some added defensive depth.
The first thing that I do now and didn't do before is to use Docker to handle all the build steps. Although Eleventy does a great job of minimising dependencies, adding a buffer between your actual machine and node dependencies, particularly indirect dependencies multiple steps down, is probably a good move these days.
Since the begining of the year there's been a rolling incident of supply chain compromises and node has suffered particularly badly there, and I just dont trust it. Since I'm using Mac, that means both the container and the Linux VM running Docker put a barrier between my machine and the node ecosystem. This also applies to the build machine when we get to GitHub actions, which might have various environment vars or organisatinal secrets you dont want inadvertant access to.
The Dockerfile I use is a simple utility container based on Node's long term support on Alpine, a distro that's very light and ideal for keeping containers small. I'd avoid using full fat distro here like Ubuntu here, even the -slim versions are bigger than Alpine containers:
FROM node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
WORKDIR /app
COPY package.json ./package.json
COPY package-lock.json ./package-lock.json
RUN npm ci --ignore-scripts
Things of note here: the docker image is pinned to a sha checksum. This is another supply chain protection that pulls a precise version rather than latest or the latest version 26. Also note it uses the npm ci with ignore scripts again to protect from supply chain issues. the ci version uses the lock file and doesnt try to bring in any updates. No scripts flag stops a common vector for the cred stealers (later versions of npm default to no scripts, but since earlier ones don't it's included here for completeness).
In terms of files from the host machine we copy in only the package json and package lock file, we'll munt the source files later.
Wrapping the Dockerfile I have a docker compose file, which I use to define the varations on build, npm audit and server. The same basic container, but doing different roles.
services:
build:
volumes:
- ./src:/app/src
- ./dist:/app/dist
- ./.eleventy.js:/app/.eleventy.js
- ./CNAME:/app/CNAME
command: npm run build
build:
context: ./
serve:
volumes:
- ./src:/app/src
- ./dist:/app/dist
- ./.eleventy.js:/app/.eleventy.js
ports:
- 8088:8080
command: npm run start
build:
context: ./
audit:
volumes:
- ./package-lock.json:/app/package-lock.json
- ./package.json:/app/package.json
command: npm audit fix
build:
context: ./ The build service mounts the various source and config elements and the target folder (dist), then runs npm run build inside the container. This is my eleventy build and is essential a sass build and the eleventy build in one. Eseentially it's npm run sass && eleventy. Serve is similar, but mounts the folders and then starts serving locally for testing, mapping the node server's 8080 port to 8088 on your local machine. You could use 8080, but if you have multiple sites having a unique ports stops conflicts.
I can run the build with:
docker compose up build
The audit container allows you to update the packages and lock files without the contents of node_modules file hitting your local machine directly.
docker compose up audit
For the GitHub actions build I now use a file like this:
name: Eleventy Build
on:
push:
branches:
- main
jobs:
build_deploy:
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: $
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Install and Build
run: |
docker compose up build
- name: Setup Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
path: dist
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0Of note here, the action only gets a limited set of permissions it needs to build and write to the pages branch. All the sub actions are also pinned to known shas to avoid malicious commits (the actions ecosystem has also had a few supply chain compromises of late). This is now using the official Github Page build actions and these partition into multiple elements of the build, setting up pages and uplaoding the build before deploy.
To get this to work you'll need to configure the main branch with permission to push to the gh-pages branch in the /settings/environments page of the GitHub repo.
For extra peace of mind you might want to limit the actions runable on your github repo to ones you specify. You can do this in /settings/actions in the allow selected actions section. If you are part of an organisation they can do this at org level too. Again depends on your risk appetite, but if you are working in a team it stops somebody bringing in a random unchecked action.
So that's the 2026 version of deploying a simple Eleventy blog via GitHub actions to GitHub pages.