GitLab CI artifacts don’t just vanish; they stick around until a policy explicitly tells them to leave.
Let’s see what this looks like in practice. Imagine a pipeline that builds a Docker image and then uploads it as an artifact.
build_docker_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t my-image:latest .
- docker save my-image:latest > my-image.tar
artifacts:
paths:
- my-image.tar
expire_in: 1 week
Here, expire_in: 1 week means that my-image.tar will be automatically deleted one week after the pipeline job finishes. This is the most common way to manage artifact lifecycles directly within your .gitlab-ci.yml.
But what if you have hundreds of jobs, each generating artifacts? Managing expiration on a per-job basis becomes tedious. This is where project-level artifact expiration policies shine. You can configure these in your GitLab project’s Settings > CI/CD > Artifacts expiration.
Here, you can define default expiration rules that apply to all jobs in the project, or create specific rules based on job names, branch names, or tags. For example, you could set a default expiration of 30 days for all artifacts, but have artifacts from a specific staging branch expire in just 7 days.
The system evaluates these policies in a specific order. When a job completes, GitLab checks for an expire_in or expire_at directive within that job’s artifact definition in the .gitlab-ci.yml. If found, that takes precedence. If not, it then looks at the project-level artifact expiration settings. If a job matches multiple project-level rules, the most restrictive rule (the one that causes the artifact to expire sooner) is applied.
Consider a scenario where you want to keep artifacts from your main branch indefinitely, but clean up everything else after 90 days.
On the project settings page, you’d configure:
- A default expiration of
90 daysfor all artifacts. - A specific rule for branches matching
mainwith an expiration ofNever expires.
This ensures that your production-related artifacts are preserved, while temporary build outputs are automatically pruned.
You can also use expire_at for specific timestamps, which is useful for time-bound promotions or releases. For instance, you might want an artifact to be available only until a specific date and time.
deploy_release_candidate:
stage: deploy
script:
- echo "Deploying release candidate..."
artifacts:
paths:
- release_notes.md
expire_at: "2024-12-31T23:59:59Z" # Expires at the end of 2024 UTC
This expire_at is a hard deadline. Once that timestamp passes, the artifact is marked for deletion, regardless of any other policies.
The actual deletion process is handled by a background worker in GitLab. It periodically scans for artifacts whose expiration time has passed and removes them. This isn’t instantaneous; there can be a delay between an artifact expiring and it being physically removed from storage.
The most surprising thing is that even if you set an artifact to "Never expires," GitLab still tracks it. It just doesn’t schedule it for deletion. This metadata is crucial for the system to know not to touch it, even when other cleanup jobs run. If you later decide to enforce an expiration, you can simply update the policy.
If you try to download an artifact that has expired, you will receive a 404 Not Found error.