While the GitLab UI and API are the standard tools for day-to-day operations, they often hit a wall when dealing with massive datasets. API rate limits can slow down bulk cleanup tasks, and the UI is ineffective for managing thousands of objects.
In such situations, we sometimes need direct, fast access to GitLab’s internal data. This is where the GitLab Rails console comes in. Built on the Ruby on Rails framework, the Rails console provides a command-line interface for interacting directly with GitLab’s application models using the Ruby language.
The Rails console is fast and flexible, but also unforgiving: a single command can modify or delete production data instantly.
It is strongly recommended to test all commands in a staging or test environment before executing them in production, and to ensure data backups are in place. Check out this post to run the GitLab using Docker/Podman locally.
At the core of the GitLab Rails console is ActiveRecord, the Object–Relational Mapping (ORM) layer provided by Ruby on Rails. ActiveRecord acts as a bridge between Ruby objects and database tables, allowing to work with database records using plain Ruby code instead of raw SQL (“just a wrapper”).
Basics
Run the rails console on your GitLab server:
sudo gitlab-rails console
Once loaded, enable the debug mode:
ActiveRecord::Base.logger = Logger.new($stdout)
Consider the following command:
prj = Project.find_by_full_path(test/test2)
Projectis a model mapped to the projects database tablefind_by_full_pathis a model method that builds a SQL query using the provided project path (try to typeProject.and press tab to initiate autocompletion, the console will display all available methods for theProjectmodel)prjis a variable to store Ruby’s object or nil (if no project found)
Project.methods.grep(/find/) will show methods available to Project model containing find word. Use <model>.methods and <object>.attributes to get all available methods and properties of object (e.g. prj.attributes and project.methods)
Next, if we run prj.container_repositories we will see actual SQL query used by the code, so we are interacting with the container_repositories table
D, [2025-12-24T09:47:41.526263 #4889] DEBUG -- : ContainerRepository Load (1.8ms) /*application:console,db_config_database:gitlabhq_production,db_config_name:main,console_hostname:gitlab.example.com*/ SELECT "container_repositories".* FROM "container_repositories" WHERE "container_repositories"."project_id" = 1 /* loading for pp */ LIMIT 11
Consider you need to find all projects with at least one tag in the repo
count = 0
Project.find_each do |project|
if project.has_container_registry_tags?
count += 1
end
end
puts "Total projects: #{count} "
and result is Total projects: 2567
Thousands of projects in the GitLab were “queried” in just a few seconds. That’s a magic of GitLab Rails console that API doesn’t have.
Some explanations:
count = 0simply a local variable; our counterProject.find_eachloads projects in batchesdo | project |starts a loop and assigns each project to the variableprojectif project.has_container_registry_tags?main logic, clear enough to anycount += 1increments our counterputs "Total projects: #{count}"prints result once the loop is finished
If you need to show project paths and print projects with image tags:
count = 0
Project.find_each do |project|
puts project.full_path
if project.has_container_registry_tags?
puts " -> Project has container registry tags"
count += 1
end
end
puts "Total projects: #{count} "
Result:
test/project1test/test2 -> Project has container registry tagstest2/test2
Try out different models such as User, Group, Ci::Pipeline and others
Zombies
An interesting behavior to note is that destroying a repository object via repo.destroy! does not automatically purge the physical data from the storage backend. If the Container Registry is active, GitLab’s background processes or a simple page refresh may trigger a “re-sync.”
- You run
repo.destroy!on repository ID16. - The database record is deleted, but the physical tags remain in storage.
- GitLab detects the existing data at that path and automatically creates a new record in a database
- A new repository appears with a new ID
17, containing all the old tags.
To prevent this “zombie” effect, you should always delete the tags through the GitLab API or Rails methods before destroying the repository record, and follow up with a Registry Garbage Collection to reclaim the disk space
gitlab-ctl registry-garbage-collect -m
#removes untagged manifests and unreferenced layers as well
#wont'work if the container registry is disabled
You can write a simple command to delete the repo and tags in the test/test2 project (DANGER):
prj = Project.find_by_full_path("test/test2")
prj.container_repositories.each do |repo|
repo.delete_tags!
repo.destroy!
end
delete_tags and has_container_registry_tags? methods require availability of GitLab Container Registry. Otherwise, the following error shown “Failed to open TCP connection to localhost:5000 (Connection refused – connect(2) for “localhost” port 5000) (Faraday::ConnectionFailed)”
Other examples
To delete image tags and container registry repositories (DANGER):
Project.find_each do |p|
next unless p.has_container_registry_tags?
p.container_repositories.each do |repo|
puts "Cleaning tags for #{p.full_path}"
repo.delete_tags!
puts ":::Destroying the repo for #{p.full_path}"
repo.destroy!
end
end
next unless can be read as “if repository doesn’t have any container registry tags, skip it and find relevant projects“; works faster and looks cleaner
If your Container registry has been disabled and you need to clean container registry repositories:
- temporary enable the container registry service (recommended) OR
- destroy registry repositories, and then manually clean file storage (based on my testing, we need to interact with /var/opt/gitlab/gitlab-rails/shared/registry/docker/registry/v2/ after deleting registry repos via GitLab rails , and likely it doesn’t break anything; blobs and repositories folders will be created automatically once you pushed any images again)
- Anyway, do you own research before any change!
To find projects with container registry repo when registry has been disabled:
count = 0
Project.find_each do |project|
if project.container_repositories.count > 0
puts " -> Project #{project.name} has container repos"
count += 1
end
end
puts "Total projects: #{count} "
To delete container registry repos when registry has been disabled (DANGER):
Project.find_each do |p|
next unless p.container_repositories.count > 0
p.container_repositories.each do |repo|
puts ":::Destroying the repo #{repo.name} for #{p.full_path}"
repo.destroy!
end
end
While the scripts above are powerful, they are also permanent. Treat the Rails console like a sharp blade: incredibly useful, but dangerous if handled carelessly. And don’t forget these rules 1. Do Your Own Research. 2. Audit with puts before execution of harmful queries (destroy and other methods) . 3. Test every query on a single project before running a batch.