Local CI#
Run the GitLab CI pipeline locally against the same
.gitlab-ci.yml your runners use. Useful for:
Verifying a change won’t break CI before pushing.
Producing and pushing a Docker image when the real CI is down.
Iterating on the pipeline itself without burning shared CI minutes.
No host installation of gitlab-ci-local, Node, or any Python
tooling is required. Everything runs in throwaway containers on
your local Docker daemon.
How it works#
./scripts/ci_local.sh launches node:22-alpine, installs
git, bash, rsync, and docker-cli, then runs the
gitlab-ci-local
npm package via npx. The host Docker socket is bind-mounted
into the container so each CI job runs as a sibling container
on your host daemon (Docker-out-of-Docker, not nested). The repo
is mounted at its real host path inside the container so sibling
jobs see the same paths as the parent.
Sensitive variables that real CI provides automatically
(CI_REGISTRY_USER, CI_REGISTRY_PASSWORD,
DOCKER_IO_USER, DOCKER_IO_PASSWORD) are read from
.gitlab_ci_local_variables.yml in the repo root. That file
is gitignored – never commit credentials.
Prerequisites#
Docker installed and running on the host.
For the build / build_branch jobs (which push to the GitLab Container Registry): a project Deploy Token (see below).
For the build_docker_hub job: Docker Hub credentials.
No prerequisites for the code stage (
vulture,ruff,typos) or the test stage (tests,tests:network).
One-time setup#
1. Create a GitLab Deploy Token#
Deploy Tokens authenticate against the project’s Container Registry without exposing your personal credentials.
Expand Deploy tokens.
Add token:
Name: anything memorable, e.g.
local-registry-push.Expiration: 6-12 months is reasonable.
Scopes: tick
read_registryandwrite_registry. Leaveread_repository/write_repositoryunchecked unless you need git access too.
Click Create deploy token. Copy both the username (typically
gitlab+deploy-token-<id>) and the token value immediately – the token is shown only once.
2. Create a Docker Hub Personal Access Token#
Only needed if you plan to run the build_docker_hub job
locally.
New Access Token. Tick Read, Write, Delete scope.
Copy the token value immediately.
3. Fill in the variables file#
The variables file is created by the initial repo checkout with
REPLACE_ME placeholders. Open it:
.gitlab_ci_local_variables.yml
Paste your values:
CI_REGISTRY: registry.gitlab.com
CI_REGISTRY_USER: gitlab+deploy-token-<your-id>
CI_REGISTRY_PASSWORD: <your-deploy-token-value>
DOCKER_IO_USER: <your-docker-hub-username>
DOCKER_IO_PASSWORD: <your-docker-hub-token>
The file is in .gitignore. Run git status after editing
– it should not appear.
Running#
The script accepts the same arguments as gitlab-ci-local.
Run every job in the pipeline#
$ ./scripts/ci_local.sh
Run a single job#
$ ./scripts/ci_local.sh tests
$ ./scripts/ci_local.sh tests:network
$ ./scripts/ci_local.sh ruff
$ ./scripts/ci_local.sh build
List available jobs without running#
$ ./scripts/ci_local.sh --list
Run a build job from a non-master branch#
The build and build_docker_hub jobs in
.gitlab-ci.yml only run on master, tag stable, or
version tags ([0-9.]+). To exercise them locally from a
feature branch, override the ref name:
$ ./scripts/ci_local.sh --variable CI_COMMIT_REF_NAME=master build
Same trick works for build_docker_hub. build_branch is
gated on merge-request pipelines:
$ ./scripts/ci_local.sh --variable CI_MERGE_REQUEST_ID=1 build_branch
What runs where#
Stage / job |
Image used |
Needs creds |
Notes |
|---|---|---|---|
|
Alpine images |
No |
Static checks. |
|
|
No |
Builds the dev image, runs Django tests with
|
|
Same as |
No |
Runs network-tagged tests. |
|
|
Yes (GitLab Deploy Token) |
Builds and pushes to |
|
Same as |
Yes (GitLab Deploy Token) |
Branch image variant. Gated by MR pipeline. |
|
|
Yes (Docker Hub PAT) |
Mirrors the image to |
Differences from real CI#
Token type. Real CI uses an ephemeral
CI_JOB_TOKEN; locally we substitute a Deploy Token..gitlab-ci.ymlreadsCI_REGISTRY_USER/CI_REGISTRY_PASSWORD(which GitLab sets automatically in CI), so the same script works in both contexts.Network egress IP. Real CI runs from data-center IPs that Yahoo Finance and other upstream APIs frequently throttle or tarpit. Locally your residential / office IP is rarely affected – network tests that flake in CI will usually pass cleanly here.
Helper utility image pulled lazily.
docker.io/firecow/gitlab-ci-local-utilis pulled the first time any test job runs –gitlab-ci-localuses it internally as a helper image. Cached after the first pull.
Performance notes#
Operation |
First run |
Cached run |
|---|---|---|
|
~10s |
~5s (no apk cache between runs) |
|
~10s |
instant (cached in named volume
|
Pulling upstream CI base images |
~1.5 GB once |
instant (host Docker daemon image cache) |
Total cold-start overhead before the first job actually runs: ~25 seconds. Subsequent runs add ~10 seconds of overhead before the first job kicks off.
Files involved#
File |
Purpose |
|---|---|
|
The actual CI pipeline definition; same file the GitLab runner uses. |
|
Compose stack the |
|
Entry-point wrapper that launches |
|
Local credentials. Gitignored. |
|
Working directory created by |