A Real GitLab + Runner
The thorough approach to local CI is to stop faking anything: run GitLab in Docker, register a runner against it, push your project to it, and watch real pipelines. Some teams already have a staging instance and use it this way.
It is not a bad instinct, and it gives you both kinds of fidelity, real execution and a real server, which is what glci is chasing too. The catch is the cost, and that most of what it buys is what you already had.
You still have to push#
This is the part that decides the comparison, and it is easy to miss while planning the setup. A local GitLab reproduces the platform. It does nothing about the loop. To run a job you still commit, still push to a remote, still wait for pipeline creation, runner pickup, container provisioning, image pull, and clone. You have removed queue time and network latency and kept everything else.
The workflow properties that make local CI worth having are absent too:
- Your uncommitted change has to become a commit before the instance can see it, so the
fix ciarchaeology comes right back. - The unit of work is a pipeline, or a retry that still schedules through the runner. You cannot run a single job on its own.
- Rules are evaluated for whatever context you actually created. Finding out what a tag pipeline produces means creating a tag, and testing a merge request pipeline means opening a merge request.
- Inspection is limited to what the UI shows you, which is the same log you would have read on gitlab.com.
A warm glci daemon does the opposite: glci run test:unit --watch re-runs one job against a working tree you have not committed, and glci show --context tag=v1.0 answers the tag question without a tag existing.
The instance is heavy#
GitLab’s own installation requirements put the baseline for a single-node install at 8 vCPU and 16 GB of RAM, with 8 GB described as the memory-constrained floor. That is Puma, Sidekiq, Gitaly, PostgreSQL, Redis, Workhorse, and Registry, all running so that you can find out whether your before_script has a typo. Boot is minutes rather than seconds, and it competes for RAM with the containers your jobs need.
glci’s resident footprint is a mock server container and one runner container, and its cold start is dominated by pulling the runner image once.
It drifts from your real instance anyway#
The less obvious failure is that a local GitLab is not your GitLab. To make pipelines behave the same you would have to mirror, and keep mirroring:
- project, group, and instance CI/CD variables, and their protected and masked flags
- protected branch and tag rules, and who can push to them
- CI/CD component and template resolution, if your pipeline uses
include: component:orinclude: project: - runner tags, so
tags:routes the way production routes - the GitLab version, because CI YAML semantics change between releases
- licensed features, since a Free instance does not behave like a Premium or Ultimate one and several stock CI templates are tier-gated
Every one of those is a drift source that produces false confidence, which is worse than no local testing because you will trust it. glci instead reads your real project’s variables from your real instance over the API, with no mirroring, and resolves variable precedence the way GitLab resolves it.
If you already maintain a staging instance for other reasons, this calculus is friendlier: the drift is someone’s job and the cost is already sunk. If you were about to stand one up for CI testing, you are taking on an ongoing maintenance obligation to shorten a loop it does not shorten.
Where a real instance is the right answer#
Some questions genuinely require one, and glci will not pretend otherwise:
- Protected variables and protected branches. glci fetches variables regardless of protection state and does not model branch protection. If your bug is “the variable is missing on unprotected branches,” you need the real rules.
CI_JOB_TOKENscope. Real job tokens are short-lived and narrowly scoped. glci forwards your personal token instead, so--no-tokentests the no-credentials path but not the real scope.- Kubernetes executor. glci is Docker-only. A pipeline that depends on the Kubernetes executor’s pod semantics has to run somewhere that has one.
- Licensed features and security scanning templates. Tier-gated behavior needs a licensed instance.
- Anything about GitLab itself, including admin settings, instance-level configuration, non-CI APIs, and upgrade rehearsals.
- Your actual runner fleet, meaning hardware, autoscaling, cloud metadata, and egress policy.
These two tools are not really competing. A staging instance answers questions about your platform; glci answers questions about your pipeline. Most days the question is about the pipeline, and it is worth being able to ask it in a second rather than an afternoon.