Stable
Run GitLab Runner with the Docker executor
Learn how to run a private GitLab Runner with the Docker executor, keep CI traffic separate from browser access and troubleshoot Package Registry failures.
Steps
Follow these in order.
- 01Separate browser and CI trafficKeep browser access protected by BusinessProxy while GitLab Runner and job containers use a private machine-to-machine route.
- 02Start and register the RunnerPersist the configuration, pin the image version, use a dedicated Docker network and enter the token interactively.
- 03Provide internal HTTPSAccept HTTPS on an internal TLS proxy and forward requests to GitLab over HTTP without exposing another host port.
- 04Switch without interrupting jobsValidate the TLS proxy, move the canonical Docker alias, update every runner entry and preserve a rollback copy.
- 05Verify and troubleshoot publishingDistinguish DNS and TLS failures from GitLab authorization errors, then verify the setup with a real Package Registry job.
Reference
Implementation details for setup and review.
- Keep GitLab external_url set to the canonical HTTPS address, for example https://gitlab.example.com.
- The GitLab container may continue listening on HTTP port 80 inside the private Docker network.
- GitLab Runner polling, repository cloning and Package Registry requests use private internal HTTPS and do not depend on browser MFA or an App Gateway session.
Browser -> BusinessProxy App Gateway -> GitLab HTTP :80
Runner/job -> internal TLS sidecar :443 -> GitLab HTTP :80- Only the sidecar owns the gitlab.example.com alias inside the Docker network.
- GitLab keeps a separate service alias such as gitlab so the sidecar can reach http://gitlab:80.
- Do not publish the sidecar with -p or ports; it is an internal CI endpoint, not another public GitLab entry point.
docker network create gitlab-internal
docker network connect --alias gitlab gitlab-internal gitlab
docker run -d --name gitlab-runner \
--restart unless-stopped \
--network gitlab-internal \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:alpine-v<tested-version>- Mounting the Docker socket gives Runner host-level control. Use a dedicated host, assign the runner only to trusted projects and restrict it to protected branches and tags.
- Set a restart policy and monitor free disk space because images, helper layers and build caches accumulate on the host.
- Do not run docker network create or docker network connect if the network or attachment already exists. Inspect the current aliases first and preserve unrelated ones.
events {}
http {
upstream gitlab_backend { server gitlab:80; }
server {
listen 443 ssl;
server_name gitlab.example.com;
ssl_certificate /certs/fullchain.pem;
ssl_certificate_key /certs/privkey.pem;
client_max_body_size 0;
location / {
proxy_pass http://gitlab_backend;
proxy_http_version 1.1;
proxy_set_header Host gitlab.example.com;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
}- Authorization, JOB-TOKEN and DEPLOY-TOKEN headers must reach GitLab unchanged. Do not replace them in the sidecar configuration.
- Validate the nginx configuration before reloading nginx after a certificate update. Do not use an expired certificate or a certificate for another hostname.
docker run -d --name gitlab-internal-tls \
--restart unless-stopped \
--network gitlab-internal \
--network-alias gitlab-internal-tls \
-v /srv/gitlab-internal-tls/nginx.conf:/etc/nginx/nginx.conf:ro \
-v /secure/certs/gitlab.example.com:/certs:ro \
nginx:1.27-alpine- Do not add -p 443:443. Runner and job containers reach the sidecar through gitlab-internal.
- Automate nginx -t and nginx -s reload after certificate renewal, and alert before certificate expiry.
curl --resolve gitlab.example.com:443:<sidecar-ip> \
https://gitlab.example.com/users/sign_in
docker network inspect gitlab-internal- Do not add -k to the preflight: the test must verify hostname and certificate trust.
- After moving the alias, restart the TLS proxy so it resolves the GitLab address again with the final network configuration.
- Expected GitLab responses are HTML 200, a normal redirect, or 401 for a protected API path, not a BusinessProxy error page.
docker exec -it gitlab-runner gitlab-runner register \
--url https://gitlab.example.com \
--executor docker \
--docker-image node:22-alpine \
--description private-docker-runner- Enter the authentication token only at the prompt and store the resulting config.toml with restricted permissions.
- Do not print authentication tokens or the full config.toml in CI logs or support tickets.
[[runners]]
url = "https://gitlab.example.com"
clone_url = "https://gitlab.example.com"
executor = "docker"
[runners.docker]
image = "node:22-alpine"
network_mode = "gitlab-internal"- The real config also contains a generated runner token; it is intentionally omitted from this example.
- Update all entries in config.toml. One stale entry can continue polling or cloning through the wrong route.
docker exec gitlab-runner getent hosts gitlab.example.com
docker exec gitlab-runner gitlab-runner verify
docker run --rm --network gitlab-internal curlimages/curl:<tested-version> \
-I https://gitlab.example.com/users/sign_in- The hostname must resolve only to the sidecar address inside gitlab-internal.
- Run a real pipeline that clones a repository and publishes or downloads a package. Treat only a successful job as end-to-end proof.
- Inspect aliases for every container in gitlab-internal and remove the canonical alias from the HTTP-only GitLab container.
- Assign the canonical alias to the TLS sidecar and restart Runner after changing config.toml.
- Do not change external_url to HTTP and do not disable certificate validation to hide the routing error.
- x509 or hostname mismatch: fix the certificate SAN, trust chain and mounted certificate; do not disable TLS verification.
- 401 Unauthorized: DNS and TLS are working; check CI_JOB_TOKEN, deploy token, project permissions and Package Registry policy.
- 404 Not Found: verify the project ID, package path, endpoint type and whether the registry is enabled for the project.
- POST /api/v4/jobs/request returns 204: Runner is healthy but GitLab has no matching pending job.
- Runner receives a job but clone or npm publish fails: confirm that helper and job containers use gitlab-internal, not only the Runner container.
- Keep the sidecar internal-only, mount keys read-only and reload only after nginx -t succeeds.
- Restrict Runner to trusted projects, protect release jobs and rotate a runner token after suspected disclosure.
- Monitor certificate expiry, Runner polling errors, failed package requests and host disk consumption.
- Keep a restricted administrative recovery path and a tested copy of the previous runner configuration.