5.1 KiB
quarto-deploy
The container image our Gitea Actions runners use to build and publish Quarto sites.
What it is
A push to a site repository triggers a runner to render the site and then copy the result to the web server. This image is the environment in which that job runs. It is deliberately small: rendering uses Quarto's frozen output, so the executed R and Python results are committed to each site repository rather than recomputed here. The image carries no language kernels; only what it takes to check out a repository, render, and sync files.
Node runs the JavaScript actions Gitea uses to bootstrap a job, such as actions/checkout. git performs the checkout, rsync copies the rendered site into place, and curl fetches Pixi baked into the image, so each run neither downloads the tool nor pipes a script into a shell.
The image stays generic about project dependencies on purpose: it ships Pixi but no packages.
The Quarto it fixes, baked in and named by the tag, is the renderer the runner uses, and each site mirrors that version in its own pixi.toml so local work matches what the runner produces.
Build
Build the image on your own machine and push it to Gitea's container registry. The runners only ever pull it.
You will need an access token with package:write access.
Go to your Gitea settings and under “Applications,” you will find “Generate New Token.”
Give the token a name like quarto-deploy-package and select “Read and Write” for package permissions.
Next, we will authenticate Docker for pushing packages by running docker login and using the newly generated token as the password.
docker login git.scient.ing
Now we can build the package for our servers, which are linux/amd64.
Note that Apple Silicon defaults to arm64, and an arm64 image may push but then fail to start on the runner with an exec-format error.
buildx cross-compiles for the platform you specify; --push sends the result straight to the registry, so the build and upload are one step, and the trailing . is the build context (the directory containing this Dockerfile).
docker buildx build --platform linux/amd64 \
-t git.scient.ing/infra/quarto-deploy:1.9.38 --push .
Bump the tag whenever the Dockerfile changes: to the new Quarto version on a Quarto bump, or with a revision suffix like :1.9.38-2 otherwise.
The image is always pulled by an explicit version, never latest, so a runner's behavior stays tied to a named artifact you can roll back to.
Quarto versions
Quarto is pinned in the image, not pulled fresh at render time.
The version lives in the Dockerfile as the QUARTO_VERSION build argument, paired with QUARTO_SHA256, the checksum of that release's linux-amd64 tarball.
The image tag carries the same number, so a runner renders against one known Quarto, and that Quarto travels with the artifact you push, pull, and roll back to.
Each site mirrors this version in its own pixi.toml for local work, so a bump happens in two places.
To move to a new Quarto, set QUARTO_VERSION in the Dockerfile to the release you want and QUARTO_SHA256 to the matching quarto-<version>-linux-amd64.tar.gz checksum from the releases page, then change the quarto pin in each site's pixi.toml to the same number.
The build verifies the download against the checksum, so a wrong or stale hash fails the build instead of shipping a Quarto you didn't intend.
Then build and push under the new version tag as shown above, point each runner's label at it, and restart the runner.
Site workflows stay untouched. They select the runner by its quarto-deploy label, never by version.
Local development
You author and test a site in a Pixi environment that pins the same Quarto image renders with, so what you see locally is what the runner publishes. Quarto comes from conda-forge:
[dependencies]
quarto = { version = "==1.9.38", channel = "conda-forge" }
Keep this equal to QUARTO_VERSION in the Dockerfile.
A few tasks cover the loop:
[tasks]
serve = { cmd = ["quarto", "preview", "content"] }
build = { cmd = ["quarto", "render", "content", "--output-dir", "../public"] }
bump = { cmd = ["bump-my-version", "bump", "patch"] }
pixi run serve previews content/ while you write.
pixi run build renders it into public/, the directory the runner publishes.
pixi run bump advances the site's own version, which is separate from the Quarto pin.
If a site executes code, add the kernels it needs (jupyter, r-base, and so on) to pixi.toml, render once, and commit the resulting _freeze so the runner reproduces the output without running anything.
Use
A runner advertises a label that maps to this image, and a workflow selects it with runs-on.
# in a runner's config.yaml
runner:
labels:
- "quarto-deploy:docker://git.scient.ing/infra/quarto-deploy:1.9.38"
# in a site repository's .gitea/workflows/deploy.yml
jobs:
deploy:
runs-on: quarto-deploy
Changing the image for each runner involves pointing the label at a new tag and restarting the runner. The workflows never change.