gitlab-ci-local
gitlab-ci-local is the best-known local GitLab CI tool and a genuinely good one: mature, actively maintained, npm install -g away, and fast. If you already use it and like it, that is a defensible position. What follows is about the single architectural difference between it and glci, since that difference is what decides which questions each tool can answer.
Where job semantics come from#
gitlab-ci-local implements execution itself. It has its own shell executor and its own Docker executor, and as its documentation says, it does not rely on gitlab-runner. glci takes the opposite route and runs the official gitlab/gitlab-runner image, supplying a fake GitLab for it to talk to.
That distinction carries more weight than it first appears to, because “run this job” is not a small behavior. It is:
- the shell script the runner generates around your
script:, including howset -einteracts with multi-line commands - the exact ordering and failure semantics of
before_script,script,after_script, andhooks: - when
pull_policydecides an image is stale, and whatif-not-presentmeans on a re-run - how
artifacts:paths:globs resolve, whatartifacts:exclude:removes, whatwhen: alwaysuploads after a failure, and howexpire_inis recorded - how
services:containers are named, aliased, health-checked, and how long the runner waits for them - which
FF_*feature flags are active and what each one changes - cache archiving, key templating,
policy: pull/push, and fallback keys - how
retry:,timeout:, and cancellation actually terminate a running container
Each item is a decision someone made inside gitlab-runner, sometimes years ago, occasionally by accident, and your pipeline may be leaning on any of them. A tool that executes jobs itself has to re-derive all of it and then keep tracking it as the runner evolves. It will get most of it right, and the leftovers are the problem: a divergence tends to surface when your pipeline does something unusual, which is when you went looking for a local reproduction in the first place.
glci holds no position on any of the above, because it is not the component doing the executing. Keeping current with the runner is a version bump of an image tag.
The half that isn’t the job#
The second difference follows from the first. Because glci had to give the runner a server to talk to, it ended up implementing one, and that server is what your pipeline’s plumbing talks to as well. glci ships a mock GitLab that provides:
- git over HTTP, so jobs clone the way they do in CI rather than getting a copied directory
- an artifact store behind the runner’s own upload and download endpoints, so
needs:/dependencies:moves artifacts the way production does, includingartifacts: falsestill propagatingdotenv - an S3-compatible cache with real AWS SigV4 auth, so cache behavior is the runner’s cache behavior
- an OCI registry answering as
$CI_REGISTRY, with pull-through caching, sodocker build && docker push $CI_REGISTRY_IMAGEworks and a later job can pull what an earlier job pushed - releases, generic packages, and tag lookups, so
release:jobs andglab-based jobs complete instead of erroring on a missing API - per-job HMAC tokens, so authentication is a real handshake
Concretely: a pipeline whose build stage pushes an image to $CI_REGISTRY and whose deploy stage pulls it back runs end to end under glci without special-casing, because both halves are talking to a registry. The same holds for Docker-in-Docker with buildx and QEMU, child and cross-project pipelines, and GitLab Pages output.
This is also why emulating the server beats emulating the runner. The server is a documented HTTP surface with a finite number of endpoints, so being complete about it is achievable. Job execution is a decade of accumulated behavior, so it isn’t.
What you pay for it#
The trade is real, and it will decide some readers against glci.
A container engine is mandatory. Docker or Podman, no exceptions, because the runner itself is a container. gitlab-ci-local’s shell executor runs jobs directly on your host, which works in locked-down environments where you cannot have a container engine at all, and it is the faster choice when your pipeline is mostly shell scripts.
Every job costs a container. glci has no shell executor, so a job that runs echo hello still starts a container. For a pipeline of trivial jobs, a host-shell runner will beat glci on wall clock.
Installation is a binary, not a package manager. curl | bash, or a release download. Lighter than a Node toolchain for some people, heavier for anyone who already has one.
glci is experimental. It is a GitLab project and labelled as such. gitlab-ci-local has years of community mileage behind it.
Neither tool supports the Kubernetes executor.
Choosing#
Reach for a host-shell tool when your pipeline is mostly scripts, you want the loop as short as physically possible, and Docker is unavailable or unwanted.
Reach for glci when the hard parts of your pipeline are the parts that aren’t your script, such as registry pushes, DinD builds, artifact and cache flow, services, matrix jobs, child pipelines, and rules: you don’t trust. The same applies when you need the local green to be evidence rather than a smoke test. That is the case glci is built for, and the reason it accepts a container engine as the price.