# Dockerizing Godot exports for GitLab CI and GitHub Actions

> godot-ci is a Docker image and a pair of CI templates that build and export Godot Engine games, then push them to GitLab Pages, GitHub Pages or Itch.io. It is a good fit for teams that want reproducible headless exports, and a poor fit for iOS releases and for anyone who treats the export preset file as disposable.

**abarichello/godot-ci** — Docker image to export Godot Engine games. Templates for Gitlab CI and GitHub Actions to deploy to GitLab Pages/GitHub Pages/Itch.io.

- Repository: https://github.com/abarichello/godot-ci
- Website: https://hub.docker.com/r/barichello/godot-ci
- Stars: 1,150 · Forks: 168
- Language: Dockerfile
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/abarichello-godot-ci

## The problem godot-ci solves: headless Godot exports

Exporting a Godot Engine game is easy on a desktop and awkward on a build server. The editor expects a display, the export templates have to be installed for the exact engine version, and a signed Android build needs a keystore that has no business sitting in a repository. godot-ci packages that environment into a container so the same export can run on a GitLab runner or a GitHub Actions runner.

The project is aimed at game developers who already have a Godot project and want releases to come out of every push or tag. The scope is export and publish, not engine development. What you get is a Docker image on Docker Hub under barichello/godot-ci, plus two reference files committed in the repository: .gitlab-ci.yml for GitLab CI and .github/workflows/godot-ci.yml for GitHub Actions. The upstream project describes them as reference templates, and the README points at a test-project folder and a game-off repository for working examples.

The appeal is repeatability. The container fixes the engine version, the export template set and the surrounding tooling in one place, so a build that worked in March does not depend on whatever the runner image happened to ship that month.

## How the image pins a Godot version and installs export templates

The Dockerfile starts from ubuntu:noble and installs the command line tools a headless export needs, among them git, git-lfs, unzip, wget, zip, adb, openjdk-17-jdk-headless, rsync, nodejs and osslsigncode. Two build arguments decide what lands in the image: GODOT_VERSION, which defaults to 4.5, and RELEASE_NAME, which defaults to stable, with SUBDIR reserved for non stable builds such as beta or release candidates.

The interesting part is how the binary and its templates are placed. The image downloads the engine archive and the export template archive from the godotengine/godot-builds releases, moves the executable to /usr/local/bin/godot, and then creates a directory that mirrors the layout Godot expects on disk:

```bash
mkdir -p ~/.local/share/godot/export_templates/${GODOT_VERSION}.${RELEASE_NAME}
```

Templates are unzipped into that same directory. Because the folder name is built from the version and the release name, changing either build argument changes where the templates land, and that is exactly the detail that the Mono instructions later depend on.

## Wiring the template into a GitLab or GitHub project

There is no install script, because there is nothing to install on the host. The image is pulled by the CI runner, and the template file is copied into the game project. The upstream project says to start by removing unused jobs and stages from the .yml file being used as a template, since a reference file full of every possible target is not a working pipeline for a single game.

Secrets then get added in the panel of the chosen service. For GitHub the project points at https://github.com/<username>/<project-name>/settings/secrets, and for GitLab at https://gitlab.com/<username>/<repo-name>/settings/ci_cd. A GitHub Pages deploy driven from GitLab CI needs three values, and their shape matters because the deploy token is embedded in the remote URL:

```bash
https://<github username>:<deploy token>@github.com/<username>/<repository>.git
```

The other two, GIT_EMAIL and GIT_USERNAME, identify the account that commits to the gh-pages branch. Everything else the pipeline relies on is supplied by the gitlab-runner as predefined variables. Once the secrets exist, the job runs the export and publishes the result, and the upstream project links live test deploys for GitHub Pages, GitLab Pages and Itch.io so you can compare a working setup against your own.

## Mono and C# projects need a second tag and a path edit

Godot builds that use Mono or C# are not handled by the standard tag, and the upstream project is explicit about the two changes required. First, the image reference moves from the plain version tag to the mono variant in .gitlab-ci.yml or godot-ci.yml, with mono-3.2.1 given as the shape of the tag. Second, the setup step has to move the Mono export templates instead of the standard ones, and the edit has to be made for both directories in the command.

The command in the template ends in the stable template directory name, and for Mono it ends in the mono template directory name:

```bash
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable.mono ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable.mono
```

Note the asymmetry that trips people up. The source path under /root is the read only copy baked into the image, while the destination under ~/.local is where the engine looks. Getting the version string wrong in only one of the two directories leaves a job that installs half the templates and then fails at export time with an error that says nothing about Mono.

## Android release builds: the keystore travels as Base64

The Android path in the template has two shapes. A debug build uses the android_debug job example, which relies on the well known debug.keystore. A release build with your own signing key is more involved, and the upstream project spells out the sequence.

The keystore file is first encoded so it can live in a CI variable. On Linux and macOS that is one command, and Windows uses a different utility:

```bash
base64 release.keystore -w 0
```

Three GitLab variables follow. SECRET_RELEASE_KEYSTORE_BASE64 holds the encoded keystore, SECRET_RELEASE_KEYSTORE_USER holds the alias, and SECRET_RELEASE_KEYSTORE_PASSWORD holds the password, all created as type variables. The pipeline then uses the android job example. Encoding the key rather than mounting it means the binary artifact exists in the CI variable store, so the protection of that secret is the whole security story here, and the project is explicit that leaked Android release keystores and Windows codesigning credentials cannot be revoked.

## Itch.io publishing runs through Butler with three secrets

Deployment to Itch.io is not done by the image itself. The project uses Butler, the official Itch.io command line tool, and the template job passes it the credentials it needs. Three variables are defined: ITCHIO_USERNAME, which matches the user segment of the personal page URL, ITCHIO_GAME, which matches the game segment of the game page URL, and BUTLER_API_KEY, an Itch.io API key that lets the CI authenticate on the account owner's behalf.

The API key carries an explicit warning in the documentation: mark it Masked in GitLab CI so it stays secret. That instruction is the whole difference between a key that is protected in the variable list and one that sits in plain text in every job log that touches it. Unlike the GitHub Pages path, which pushes to a branch through a git remote, the Itch.io path is an upload performed by an external client, so the same repository produces two quite different deploy mechanisms depending on the target.

## What godot-ci deliberately leaves out: iOS, modules and GDNative

Two directions are closed off and the project says so rather than leaving users to guess. iOS is marked as not available yet, with the reasoning given plainly: automating Xcode projects is doable but not trivial, and macOS runners only recently became available for GitHub Actions, so it will happen eventually. Anything expecting a signed iOS artifact from this pipeline today is disappointed.

Custom engine modules are a second boundary. The Godot binary in the image has to be compiled with the modules you need beforehand, and the project points at a separate repository by Calinou for automating Godot builds, then at a guide on using a custom Godot build for automated exports. Once that custom build exists, you use it to export as usual. GDNative and C++ compilation is likewise delegated to another repository built on top of this one.

So the honest scope is a standard Godot build, stable or otherwise, exporting with the standard template set. The project also ships a gitea-godot-ci.yml file at the top level, and a mono.Dockerfile, and a getbutler.sh helper, which suggests the template approach has been extended by the author in directions the README does not describe in detail.

## The export preset file is the single most common failure point

The troubleshooting section of the upstream project is short and pointed, and it identifies the same problem repeatedly: the export preset file has to be committed to version control, which in practice means export_presets.cfg must not be in .gitignore. Yet the same section warns against committing the Android release keystore or Windows codesigning credentials, so the file is required in the repository while its neighbours must stay out of it.

Two further checks are listed. The export names in export_presets.cfg have to match the names used in the CI script, and the match is case sensitive. Preset names containing spaces must be written inside quotes, either single or double. And the paths in the CI script have to be right relative to where the project lives, which matters for people who keep the game inside a subfolder the way the test-project template does.

These are mundane checks, and they are the entire troubleshooting surface. There is no mention of a rollback procedure, no retry policy, and no documented way to reproduce a failed export locally other than running the same image.

## Conclusion

Adopt godot-ci if you ship a Godot project whose export presets are already committed and reviewed, because that file is the part of the pipeline that silently breaks builds. Do not adopt it expecting iOS support, custom module builds, or a managed GDNative toolchain, since none of those ship here. Before the first run, check that export_presets.cfg is tracked in version control, that the preset names in that file match the CI script character for character, and that the image tag you pin matches the Godot version your project expects.

## FAQ

### How do I build a Godot Mono (C#) project with godot-ci?

Change the image tag in .gitlab-ci.yml or godot-ci.yml from barichello/godot-ci:VERSION to barichello/godot-ci:mono-VERSION, and change the setup step to move the export templates ending in ${GODOT_VERSION}.stable.mono in both directories of the command.

### Which CI services and deploy targets does godot-ci support?

It ships templates for GitLab CI and GitHub Actions, and the GitLab template covers Godot exports, GitHub Pages, GitLab Pages and Itch.io. iOS is explicitly not available yet.

### Why does my export fail in a godot-ci pipeline?

The upstream project points at three causes: export_presets.cfg missing from version control, export preset names that do not match the CI script case sensitively, and paths in the CI script that are wrong for the directory the project sits in.

## Sources

- [abarichello/godot-ci on GitHub](https://github.com/abarichello/godot-ci)
- [License: MIT](https://github.com/abarichello/godot-ci/blob/master/LICENSE)
- [Project website](https://hub.docker.com/r/barichello/godot-ci)
- [README](https://github.com/abarichello/godot-ci/blob/master/README.md)
- [Releases](https://github.com/abarichello/godot-ci/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/abarichello-godot-ci
