docker for development and build of 11ty project
Feb 4, 2025
Introduction
This post contains my notes on using docker to develop and build production versions of this 11ty (aka eleventy) site. A primary motivation for this work is to easily use node.js in a container and to avoid having to install, update, etc node.js on my local machine.
In this post I will assume that docker is installed and can be run without sudo. See the previous post for information on installing docker on Pop!_OS or Ubuntu if you haven't done this. By default, docker needs root access to function and requires sudo. Of course, it is possible to use docker as a non-root user as documented in the previous post.
The are many, many details when using docker. I will focus on the things that I found to make 1tty development and production with docker quick and efficient for me. For a more complete introduction try this youtube video by JavaScript Mastery and associated github repo . I found them very helpful!
A simple node.js Dockerfile
It is not necessary to use a Dockerfile to use docker, but I have found these files to be a good starting point. So, let's create a simple example that uses a node.js version 20 image. We do this by putting the following lines a file named Dockerfile:
# set the base image
FROM node:20-alpine
# set working directory
WORKDIR /app
This file tells docker to pull the Alpine Linux image with node.js 20 installed
from the docker.io library and to set /app
as the working directory. The first step is to create an image
that we tag with the name "node20-image":
$ docker build -t node20-image .
[+] Building 0.9s (6/6) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 117B 0.0s
=> [internal] load metadata for docker.io/library/node:20-alpine 0.6s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/2] FROM docker.io/library/node:20-alpine@sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b 0.0s
=> CACHED [2/2] WORKDIR /app 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:9773d626e6502c70b44cd69676b28ef81ba3ecff88c396877e4372c4f7e4bf79 0.0s
=> => naming to docker.io/library/node20-image 0.0s
Note the "." at the end of the command. This tells docker to build the image "here", in the local directory. A list of the docker images on the local machine can be obtained using (I only show the image created above):
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
node20-image latest 9773d626e650 5 days ago 133MB
To "run" the image in a container we use the following to get an interactive sh terminal:
$ docker run -it node20-image sh
/app #
A little investigating shows there is nothing in the /app directory, but there are signs of the minimal Alpine Linux install when we list the contents of the root directory. We test the version of node installed and exit:
/app # ls
/app # ls /
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/app # node --version
v20.18.2
/app # exit
$
Build the eleventy-base-blog
Next up, let's use the base blog from 11ty to build up a Dockerfile that installs 11ty and runs one of the scripts in the package.json file. First, make a directory to use for experimentation and clone the blog repository from github:
$ mkdir docker-play
$ cd docker-play/
$ git clone https://github.com/11ty/eleventy-base-blog.git .
$ l
_config/ _data/ _includes/ netlify.toml public/ vercel.json
content/ eleventy.config.js LICENSE package.json README.md
If we open and inspect the package.json file we can see the scripts that have been defined for the "base blog":
##
## package.json -- just the scripts definitions
##
"scripts": {
"build": "npx @11ty/eleventy",
"build-nocolor": "cross-env NODE_DISABLE_COLORS=1 npx @11ty/eleventy",
"build-ghpages": "npx @11ty/eleventy --pathprefix=/eleventy-base-blog/",
"start": "npx @11ty/eleventy --serve --quiet",
"start-ghpages": "npx @11ty/eleventy --pathprefix=/eleventy-base-blog/ --serve --quiet",
"debug": "cross-env DEBUG=Eleventy* npx @11ty/eleventy",
"debugstart": "cross-env DEBUG=Eleventy* npx @11ty/eleventy --serve --quiet",
"benchmark": "cross-env DEBUG=Eleventy:Benchmark* npx @11ty/eleventy"
},
It looks like the npm run start
and npm run build
are the commands that
we will want to execute with docker. We start by creating a Dockerfile that
focuses on the build command. This follows the ideas presented by JavaScript
Mastery, linked above, and includes creating a non-root user to do most of
the work:
## Dockerfile.build
# set the base imaage
FROM node:20-alpine
# create an app group and user
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /app
# copy package.json and package-lock.json to image
COPY package*.json ./
# change the ownership of the /app directory
# to the app user and group
USER root
RUN chown -R app:app .
USER app
# install 11ty dependencies
RUN npm install
# copy files to /app in container
COPY . .
# run build command for production
RUN npm run build
Because we've named this file Dockerfile.build, we have to tell the build command the filename, like so:
$ docker build -f Dockerfile.build -t 11ty-build .
[+] Building 18.7s (13/13) FINISHED docker:default
=> [internal] load build definition from Dockerfile.build 0.1s
=> => transferring dockerfile: 551B 0.0s
=> [internal] load metadata for docker.io/library/node:20-alpine 0.7s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [1/8] FROM docker.io/library/node:20-alpine@sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b4 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 1.25MB 0.0s
=> CACHED [2/8] RUN addgroup app && adduser -S -G app app 0.0s
=> CACHED [3/8] WORKDIR /app 0.0s
=> [4/8] COPY package*.json ./ 0.1s
=> [5/8] RUN chown -R app:app . 0.3s
=> [6/8] RUN npm install 14.2s
=> [7/8] COPY . . 0.1s
=> [8/8] RUN npm run build 2.1s
=> exporting to image 0.6s
=> => exporting layers 0.6s
=> => writing image sha256:827ed06e361621227eb5a6e007aecbb55fbecb90a58e66262755a43490ebbdcc 0.0s
=> => naming to docker.io/library/11ty-build 0.0s
If you run a docker image ls
, you should now
see 11ty-build among the images.
Runnning the 11ty-build image
If we try running the image, it seems to run and exit quickly:
$ docker run 11ty-build
$
We can run docker ps -a
to verify that the
container ran and exited.
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bcafa52e7337 11ty-build "docker-entrypoint.s…" 6 seconds ago Exited (0) 6 seconds ago elegant_jang
We can check for the output of the 11ty build in the local _site directory, but it's not there!
$ ls
_config _data eleventy.config.js LICENSE package.json README.md
content Dockerfile.build _includes netlify.toml public vercel.json
$
What's up? It turns out that 11ty is installed and the project is built inside of the container. We can see this by running in "interactive" mode:
$ docker run -it 11ty-build sh
/app $ ls
Dockerfile.build README.md _data _site eleventy.config.js node_modules package.json vercel.json
LICENSE _config _includes content netlify.toml package-lock.json public
/app $ ls _site/
404.html about blog css dist feed img index.html sitemap.xml tags
/app $ exit
$
Using ls
we can see that all of the files from
the local directory are in the container and the _site
directory,
with the build output are there. How do we get that out of the container
and on to the host machine?
Let's start by looking at the image and container that we have already created above:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
11ty-build latest 827ed06e3616 21 hours ago 212MB
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e7b735bd5773 11ty-build "docker-entrypoint.s…" 12 minutes ago Exited (0) 12 minutes ago thirsty_liskov
We can see that there is our 11ty-build image and a container, based on that image, that has been given the random name thirsty_liskov. The random name makes copying the built site out of the container more complicated. Let's create a container with the name 11ty-container so that we can easily reference it:
$ docker run --name 11ty-container 11ty-build
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e6b3f6472579 11ty-build "docker-entrypoint.s…" 4 seconds ago Exited (0) 3 seconds ago 11ty-container
e7b735bd5773 11ty-build "docker-entrypoint.s…" 14 minutes ago Exited (0) 14 minutes ago thirsty_liskov
Next, let's
- list the directory contents, showing no
_site
directory, - copy the
_site
directory from the container to the host machine, - and finally, list the directory, show the
_site
directory and its contents.
$ l
_config/ content/ _data/ Dockerfile.build eleventy.config.js _includes/ LICENSE netlify.toml package.json public/ README.md vercel.json
$ docker cp 11ty-container:/app/_site _site
Successfully copied 437kB to /home/username/Sandbox/docker-play/_site
$ l
_config/ content/ _data/ Dockerfile.build eleventy.config.js _includes/ LICENSE netlify.toml package.json public/ README.md _site/ vercel.json
$ l _site
404.html about/ blog/ css/ dist/ feed/ img/ index.html sitemap.xml tags/
$
Yay, it worked! I highly recommend you try all of above yourself if you are learning docker-- it helped me.
tldr; build with docker
It is worth listing the essential commands from above. These could be added to a bash/python/etc... script and executed with a single command. For example, we could do:
# create 11ty-build image
$ docker build -f Dockerfile.build -t 11ty-build .
# create 11ty-container
$ docker run --name 11ty-container 11ty-build
# copy _site directory to host
$ docker cp 11ty-container:/app/_site _site
# [optional] cleanup image and container
$ docker rmi 11ty-build:latest
$ docker container rm 11ty-container
Note, this does depend on the Dockerfile.build file listed above.
Development with the eleventy-base-blog
The elements above will allow me, or you, to build the 11ty demo blog. After
the build, all of the static content is located in the _site
directory and can be deployed to any web server.
Development of the site, on a local machine requires a variety of things that make the docker setup more complicated:
- the host source directory has to be "watched" for changes, and
- the 11ty development server needs to be able to communicate from the container to the host
The starting point is a Dockerfile almost exactly the same as the build
example above. I will call this one Dockerfile.dev
## Dockerfile.dev
# set the base imaage
FROM node:20-alpine
# create an app group and user
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /app
# copy package.json and package-lock.json to image
COPY package*.json ./
# change the ownership of the /app directory to the app user
USER root
RUN chown -R app:app .
USER app
# install 11ty dependencies
RUN npm install
# copy files to /app in container
COPY . .
# expose container port 8080
EXPOSE 8080
Notice that port 8080 is exposed and an npm script is NOT run. To use this
Dockerfile and start up the 11ty development server with "watch for changes"
ability we use docker compose
with the following
compose.yaml
file:
services:
development:
build:
context: .
dockerfile: Dockerfile.dev
image: 11ty-dev
command: ["npm", "run", "start"]
ports:
- 8080:8080
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules
- action: rebuild
path: package.json
With both of the above files in place:
$ ls
compose.yaml _config content _data Dockerfile.build Dockerfile.dev eleventy.config.js _includes LICENSE netlify.toml package.json public README.md vercel.json
The image and container can be started using the command
docker compose up --watch
$ docker compose up --watch
[+] Running 1/1
! development Warning pull access denied for 11ty-dev, repository does not exist or may require 'docker login': denied: requested access to the resource is den... 1.0s
[+] Building 0.8s (13/13) FINISHED docker:default
=> [development internal] load build definition from Dockerfile.dev 0.0s
=> => transferring dockerfile: 475B 0.0s
=> [development internal] load metadata for docker.io/library/node:20-alpine 0.5s
=> [development internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [development 1/7] FROM docker.io/library/node:20-alpine@sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b4 0.0s
=> [development internal] load build context 0.0s
=> => transferring context: 3.92kB 0.0s
=> CACHED [development 2/7] RUN addgroup app && adduser -S -G app app 0.0s
=> CACHED [development 3/7] WORKDIR /app 0.0s
=> CACHED [development 4/7] COPY package*.json ./ 0.0s
=> CACHED [development 5/7] RUN chown -R app:app . 0.0s
=> CACHED [development 6/7] RUN npm install 0.0s
=> CACHED [development 7/7] COPY . . 0.0s
=> [development] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:77fc8e95c6e709749aaf5085e0d342585eae7b6b5fb486dbe3cff369abf1cfd4 0.0s
=> => naming to docker.io/library/11ty-dev 0.0s
=> [development] resolving provenance for metadata file 0.0s
[+] Running 2/2
✔ development Built 0.0s
✔ Container docker-play-development-1 Created 0.1s
Attaching to development-1
⦿ Watch enabled
development-1 |
development-1 | > eleventy-base-blog@9.0.0 start
development-1 | > npx @11ty/eleventy --serve --quiet
development-1 |
development-1 | [11ty] Copied 5 Wrote 16 files in 0.34 seconds (v3.0.0)
development-1 | [11ty] Watching…
development-1 | [11ty] Server at http://localhost:8080/
Open a web browser and go to
http://localhost:8080/
to see the
11ty demo blog. Hit CNTRL-C
to exit the server and end the
session. While the server is up, the site should rebuild when changes are
made to the 11ty site code, like working on a blog post. Everyime you save
a file the site should rebuild and display the changes. Nice.
Note:
There is a "Gotcha" here. Make sure that the
_site
directory is removed on the host computer BEFORE
running the docker compose command! For example, you could do the following
to be safe:
$ rm -r _site/
$ docker compose up --watch
If you don't remove _site
directory before running docker
compose the host and container versions of this directory will clash and
you will get confusing "permission" errors in the output.
Wrapping up
So, that's everything I know!? I am currently using this setup to develop and build this site with 11ty version 3.0 and node version 20. The version of node and npm are defined at the top of the Dockerfile and can be changed any time node.js needs to be updated or changed.
If you have ideas to improve this approach. Or, you get stuck, send me an email at the address in the footer. I'll try to be helpful if I have the time.