A job stuck pending, two deploys racing, and a Docker daemon that is not there
An instance has four runners registered and nobody has changed the runner configuration in a quarter: docker-a (docker executor, tags docker, run_untagged = false), shell-legacy (shell executor on the production host, tags deploy, concurrent = 1 in config.toml), k8s-small (Kubernetes executor, tags k8s, limit = 2), and a group runner with run_untagged = true and no tags. Three problems: the unit job, which defines no tags:, sits pending for 20 minutes and then fails; two pipelines triggered 40 seconds apart both ran deploy:prod and a schema migration deadlocked against itself; and docker build inside build:image on docker-a fails with Cannot connect to the Docker daemon at tcp://docker:2375.
1. Walk through how you would diagnose the pending `unit` job, and what you would change.
Read the worked answer
Pending means no runner has picked the job up; it is a scheduling problem, not a script problem. Read the failure text first, because GitLab tells you which case you are in: "no runners that match all of the job's tags" is a tag mismatch; "no active runners online" means runners exist but are paused, offline, or have no capacity left.
Checklist, in order:
- Tags. A job with no
tags:can only run on a runner withrun_untagged = true.docker-ahasrun_untagged = falseandk8s-smallis tagged, so the only eligible runner is the group runner - and if that runner is paused, at itslimit, or belongs to a group the project is not in, the job waits forever. - Capacity and limits. Runner
limit, instanceconcurrent, and queued jobs onshell-legacy(concurrent = 1) can serialise everything behind one long job. - Protected status. A protected runner only accepts jobs on protected branches; an unprotected feature branch job waits indefinitely with no obvious message.
- Rules. Confirm the job exists in the pipeline at all - a
rulesmismatch removes it from the pipeline instead of queueing it.
Fix: be explicit. Add tags: [docker] to unit so it lands on a known runner, and keep run_untagged = false on project-specific runners so future untagged jobs cannot silently monopolise them. Tagging is the durable fix; flipping run_untagged on every runner is how tag hygiene collapses.
2. Both pipelines deployed within a minute of each other and the migration deadlocked. What configuration prevents this, and what does it not solve?
Read the worked answer
Use resource_group on the job. Jobs that share a resource group never run concurrently: the second one waits until the first finishes, then proceeds.
deploy:prod:
stage: deploy
resource_group: production
environment:
name: production
script:
- ./migrate.sh
- ./deploy.sh
It is keyed by name and scoped to the project, so the same name in two projects does not serialise across them - pick names per real-world resource (production-db, production). Note it serialises, it does not cancel: a stale pipeline will still deploy after the newer one, in arrival order.
What it does not solve, and what to layer on:
- Stale pipelines deploying at all. Add
interruptible: trueand enable auto-cancel of redundant pipelines, or require a manualwhen: manualgate on the deploy job. - Unauthorised deploys. Use protected environments with allowed users/approvals so the job cannot start without a human.
- Migration safety itself. Run migrations as a separate serialised job with an advisory lock in the database;
resource_grouponly guarantees GitLab does not start two of them at once. - Runner capacity.
shell-legacyatconcurrent = 1already serialises, which is why the deadlock was surprising - the two jobs ran on different runners, or two runner processes were started from the same config.
3. Explain the `tcp://docker:2375` failure and give a working configuration, plus the alternative when privileged mode is unavailable.
Read the worked answer
docker build is talking to a Docker daemon that does not exist on that socket. In a docker-executor job, docker is only the service hostname if a dind service is declared, and current docker:dind images require TLS by default on port 2376 - with TLS enabled, the unencrypted 2375 listener is off, so the client's connect attempt is refused.
build:image:
tags: [docker]
image: docker:24-cli
services:
- name: docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
before_script:
- docker info
Two prerequisites people miss: the runner must have privileged = true under [runners.docker] in config.toml (registration flag --docker-privileged), and DOCKER_TLS_CERTDIR must be identical for the client and the service so the shared cert volume resolves to the same path.
If privileged mode is not available - typical on Kubernetes executors and shared SaaS runners - do not try to force dind. Use a daemonless image builder: Kaniko, Buildah, or docker buildx against a remote BuildKit daemon. These work unprivileged (Kaniko and rootless Buildah with appropriate settings) and avoid handing every job root on the host.