Daggerify the Blog - Part 1: Build
The original deployment strategy (detailed here) for this blog was manual and error prone. The ideal goal is to use dagger to build out the whole CI/CD system; from building the image to deploying on a local Kubernetes cluster.
Part 1 covers building the docker image and pushing it to the Github container registry.
Building
The container image can be broken into a few parts.
- The Hugo extended binary.
- The Node and NPM binaries.
- The theme and its dependencies.
- The actual content of the blog.
Hugo Extended
Dagger provides a way to build a go binary using the Dagger Universe Go package.
Each of the following is an action defined with a dagger plan.
Base Go 1.18 image
First a base image with Go 1.18 need to be created. It is defined as the
disjunction between the go.#Image
struct, and the
{version: "1.18"}
struct. The output of this step, defined by go.#Image
,
can be used by other actions; similar to multi-stage docker builds.
package main
import (
"dagger.io/dagger"
"universe.dagger.io/go"
)
dagger.#Plan & {
actions: {
_baseGo: go.#Image & {
version: "1.18"
}
}
}
Note: The underscore in
_baseGo
marks it as a hidden field.
Pull Hugo Source code
The Hugo source can be pulled in using the Dagger git package.
package main
import (
"dagger.io/dagger"
"universe.dagger.io/git"
)
dagger.#Plan & {
actions: {
_hugoSource: git.#Pull & {
remote: "https://github.com/gohugoio/hugo.git"
ref: "v0.96.0"
}
}
}
Build Hugo
This action utilizes the output of the previous two actions to build the hugo
binary. Notice how _hugoSource.output
and _baseGo.output
are passed to
the go.#Build
struct.
package main
import (
"dagger.io/dagger"
"universe.dagger.io/go"
"universe.dagger.io/git"
)
dagger.#Plan & {
actions: {
_baseGo: go.#Image & {
version: "1.18"
}
_hugoSource: git.#Pull & {
remote: "https://github.com/gohugoio/hugo.git"
ref: "v0.96.0"
}
_hugoBin: go.#Build & {
source: _hugoSource.output
container: go.#Container & {input: _baseGo.output}
}
}
}
Base dependencies
Create a new image using universe.dagger.io/apline.#Build
with the packages that we need.
Note: We could specify the versions needed (replace the underscore) but alpine already pins package versions to the distribution version. See Issue #1532 for more info.
dagger.#Plan & {
actions: {
_base: alpine.#Build & {
packages: {
"npm": _
"go": _
"git": _
}
}
}
}
Putting it all Together
This last action is the biggest. It is a series of steps in docker.#Build
where each step can be thought of a line in a Dockerfile. I am going to explain
each step in order, then display the completed action at the end.
Copy Hugo Binary and Theme Files
Starting with the image generated from the _base
step,
copy the hugo binary to /bin/hugo
, then copy the theme files
to blog/themes/gruvbox/
.
dagger.#Plan & {
actions: {
build: docker.#Build & {
steps: [
_base,
docker.#Copy & {contents: _hugoBin.output, dest: "/bin/"},
docker.#Copy & {contents: _theme.output, dest: "/blog/themes/gruvbox/"},
...
]
}
}
}
Copy sum
and lock
Files
Next, copy over various package definition files. These are copied over before the rest of the content to utilize the builtin caching.
dagger.#Plan & {
client: {
filesystem: "./": read: {
contents: dagger.#FS
exclude: ["node_modules", "public", "build.cue", "cue.mod", "themes", ".envrc"]
}
}
actions: {
build: docker.#Build & {
steps: [
...
docker.#Copy & {
contents: client.filesystem."./".read.contents
include: ["go.mod", "go.sum", "package.json", "package-lock.json", "package.hugo.json", "config.toml"]
dest: "/blog/"
},
...
]
}
}
}
Download and Install Dependencies
A fairly self explanatory set of steps.
dagger.#Plan & {
actions: {
build: docker.#Build & {
steps: [
...
docker.#Run & {
workdir: "/blog/"
command: {name: "hugo", args: ["mod", "get"]}
},
docker.#Run & {
workdir: "/blog/"
command: {name: "hugo", args: ["mod", "npm", "pack"]}
},
docker.#Run & {
workdir: "/blog/"
command: {name: "npm", args: ["install"]}
},
...
]
}
}
}
Copy Blog Content
Once again a simple step. Just copying frequently changed content files into the image.
dagger.#Plan & {
actions: {
build: docker.#Build & {
steps: [
...
docker.#Copy & {
contents: client.filesystem."./".read.contents
dest: "/blog/"
},
...
]
}
}
}
Set Configuration
This step sets a view values defined here. workdir
is self
explanatory, it sets the working directory for the cmd
option. cmd
is the
command that runs by default. In other words, when docker run $IMG_NAME
is
run it starts the container and calls the command defined by cmd
.
Lastly label
is used to connect the entry on the Github Container registry to
the github repository where the source code is stored.
dagger.#Plan & {
actions: {
build: docker.#Build & {
steps: [
...
docker.#Set & {config: {
workdir: "/blog/"
cmd: ["/bin/hugo", "server", "--bind=0.0.0.0"]
label: "org.opencontainers.image.source": "https://github.com/kgb33/blog.kgb33.dev"
}},
...
]
}
}
}
Local convenience Actions
dagger do local load
loads the image into the local docker registry.
dagger do local run
automatically runs the hugo server. Although this will
cause dagger to hang because the action never completes.
dagger.#Plan & {
actions: {
local: {
load: cli.#Load & {
image: build.output
tag: "blog.kgb33.dev:latest"
host: client.network."unix:///var/run/docker.sock".connect
}
// Unsure how to detach from container Currently dagger 'hangs'
// while running the hugo server. Cancel via <Ctrl-C>
run: cli.#Run & {
cli.#RunSocket & {
host: client.network."unix:///var/run/docker.sock".connect
}
input: build.output
}
}
}
}
Complete Actions
package main
import (
"dagger.io/dagger"
"universe.dagger.io/docker"
"universe.dagger.io/docker/cli"
"universe.dagger.io/alpine"
"universe.dagger.io/go"
"universe.dagger.io/git"
)
dagger.#Plan & {
client: {
filesystem: "./": read: {
contents: dagger.#FS
exclude: ["node_modules", "public", "build.cue", "cue.mod", "themes", ".envrc"]
}
env: GHCR_PAT: dagger.#Secret
network: "unix:///var/run/docker.sock": connect: dagger.#Socket
}
actions: {
_baseGo: go.#Image & {
version: "1.18"
packages: {
"gcc": _
"g++": _
}
}
_hugoSource: git.#Pull & {
remote: "https://github.com/gohugoio/hugo.git"
ref: "v0.96.0"
}
_hugoBin: go.#Build & {
source: _hugoSource.output
container: go.#Container & {input: _baseGo.output}
tags: "extended"
env: "CGO_ENABLED": "1"
}
_base: alpine.#Build & {
packages: {
"npm": _
"go": _
"git": _
}
}
_theme: git.#Pull & {
remote: "https://github.com/schnerring/hugo-theme-gruvbox.git"
ref: "main"
}
build: docker.#Build & {
steps: [
_base,
docker.#Copy & {contents: _hugoBin.output, dest: "/bin/"},
docker.#Copy & {contents: _theme.output, dest: "/blog/themes/gruvbox/"},
docker.#Copy & {
contents: client.filesystem."./".read.contents
include: ["go.mod", "go.sum", "package.json", "package-lock.json", "package.hugo.json", "config.toml"]
dest: "/blog/"
},
docker.#Run & {
workdir: "/blog/"
command: {name: "hugo", args: ["mod", "get"]}
},
docker.#Run & {
workdir: "/blog/"
command: {name: "hugo", args: ["mod", "npm", "pack"]}
},
docker.#Run & {
workdir: "/blog/"
command: {name: "npm", args: ["install"]}
},
docker.#Copy & {
contents: client.filesystem."./".read.contents
dest: "/blog/"
},
docker.#Set & {config: {
workdir: "/blog/"
cmd: ["/bin/hugo", "server", "--bind=0.0.0.0"]
label: "org.opencontainers.image.source": "https://github.com/kgb33/blog.kgb33.dev"
}},
]
}
publish: docker.#Push & {
dest: "ghcr.io/kgb33/blog.kgb33.dev"
image: build.output
auth: {username: "kgb33", secret: client.env.GHCR_PAT}
}
local: {
load: cli.#Load & {
image: build.output
tag: "blog.kgb33.dev:latest"
host: client.network."unix:///var/run/docker.sock".connect
}
// Unsure how to detach from container Currently dagger 'hangs'
// while running the hugo server. Cancel via <Ctrl-C>
run: cli.#Run & {
cli.#RunSocket & {
host: client.network."unix:///var/run/docker.sock".connect
}
input: build.output
}
}
}
}