GitLab container registry push fails - expired token or 401
docker push to registry.gitlab.com fails with 'unauthorized' or 401
Focused incident mode — strips away everything but this runbook.
Symptoms
- docker push to registry.gitlab.com fails with 'unauthorized' or 401
- CI job log shows 'denied: access forbidden' or 'authentication required'
- Image build succeeds but push step fails at the registry
GitLab deploy tokens expire on the date set during creation. If no expires_at is specified, they never expire - but they can still be revoked manually.
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Docker login output
glab ci trace <PIPELINE_ID> --job <JOB_ID> | grep -E 'login|unauthorized|denied' | head -10Registry credential configuration
glab api /projects/<PROJECT_ID>/variables --jq '.[] | select(.key | test("REGISTRY|TOKEN")) | .key'
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- Token was created > 90 days ago and has not been rotated
- docker login fails with 401
Confirm before fixing
glab api /projects/<PROJECT_ID>/deploy_tokens --jq '.[] | {name, expires_at, active}'Check deploy token expiry
Fix
glab api -X POST /projects/<PROJECT_ID>/deploy_tokens -f name='new-deploy-token' -f scopes='write_registry' -f expires_at='2026-12-31'Create a new deploy token with write_registry scope
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
glab ci trace <PIPELINE_ID> --job <JOB_ID> | grep -E 'login|unauthorized|denied' | head -10Read the exact error
echo $REGISTRY_PASSWORD | docker login registry.gitlab.com -u <USERNAME> --password-stdinTest credentials
glab api /projects/<PROJECT_ID>/deploy_tokens --jq '.[] | {name, expires_at, scopes}'Check token expiry and scopes
glab api -X POST /projects/<PROJECT_ID>/deploy_tokens -f name='new-token' -f scopes='write_registry'Create a fresh deploy token if needed
Prevention
- Set a calendar reminder to rotate deploy tokens before they expire.
- Use CI/CD job tokens (CI_JOB_TOKEN) for intra-project registry pushes instead of PATs.
- Always create tokens with the minimum required scope (write_registry only when pushing).
- Audit deploy token usage quarterly and delete unused tokens.
Related runbooks
Same stack, same failure family — worth a skim.
Docker push to ghcr.io fails with 'denied: permission_denied' or '403 Forbidden'
Secret appears in plaintext in the job log instead of being masked
Downstream pipeline does not start when the trigger API is called
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.