A GitHub Action to automatically clean up old Docker tags from Docker Hub repositories, helping you manage storage costs and keep your repositories organized.
- 🗑️ Automated Cleanup: Remove old PR tags, branch SHA tags, and custom patterns
- 🛡️ Protected Tags: Never delete important tags (latest, main, semantic versions)
- 🔐 Secure Authentication: Works with both Docker Hub passwords and Personal Access Tokens
- 📊 Detailed Reporting: Get summaries and statistics of cleanup operations
- 🎮 Flexible Control: Dry-run mode, custom retention periods, and pattern matching
- 🔄 Multiple Repositories: Clean multiple Docker repositories in a single run
- 📈 Output Metrics: Access cleanup statistics as action outputs for further processing
name: Docker Cleanup
on:
schedule:
- cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC
workflow_dispatch: # Allow manual trigger
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Cleanup
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'image1,image2,image3'
dry-run: false # Set to true for testing- name: Docker Hub Cleanup
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
organization: 'my-org' # Optional: defaults to username
repositories: 'app-frontend,app-backend,app-worker'
pr-retention-days: 30 # Keep PR tags for 30 days
sha-retention-days: 14 # Keep SHA tags for 14 days
dry-run: false
verbose: true # Enable detailed logging
protected-tags: 'staging,production' # Additional protected tags
custom-patterns: '{"^feature-.*": 7, "^hotfix-.*": 3}' # Custom patterns with retention daysYou can clean repositories from different organizations by specifying the full namespace:
- name: Docker Hub Cleanup
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Mix repositories from different namespaces
repositories: 'myorg/frontend,mycompany/backend,personal-repo'
dry-run: falseWhen specifying repositories:
- Use
namespace/repositoryformat to access repositories in specific organizations - Use just
repositoryto use the default namespace (organization input or username) - You can mix both formats in the same repositories list
Add these secrets to your repository:
- Go to Settings → Secrets and variables → Actions
- Add the following secrets:
DOCKERHUB_USERNAME: Your Docker Hub usernameDOCKERHUB_PASSWORD: Your Docker Hub password or Personal Access Token
For enhanced security, use a Docker Hub Personal Access Token instead of your password:
- Log in to Docker Hub
- Go to Account Settings → Security → Personal Access Tokens
- Click Generate New Token
- Give it a descriptive name (e.g., "GitHub Actions Cleanup")
- Select appropriate permissions:
- For public repositories:
Public Repo Write - For private repositories:
Private Repo Write
- For public repositories:
- Copy the token and save it as
DOCKERHUB_PASSWORDin GitHub Secrets
| Input | Description | Required | Default |
|---|---|---|---|
username |
Docker Hub username | ✅ | - |
password |
Docker Hub password or Personal Access Token | ✅ | - |
organization |
Default Docker Hub organization/namespace | ❌ | username |
repositories |
Comma-separated list of repository names (can include namespace) | ✅ | - |
pr-retention-days |
Days to retain PR tags (pr-*) | ❌ | 30 |
sha-retention-days |
Days to retain branch SHA tags | ❌ | 14 |
dry-run |
Preview deletions without executing | ❌ | true |
verbose |
Enable verbose logging | ❌ | false |
protected-tags |
Additional tags to protect | ❌ | '' |
custom-patterns |
JSON string of custom patterns | ❌ | '{}' |
| Output | Description | Example |
|---|---|---|
deleted-count |
Total number of tags deleted | 42 |
identified-count |
Tags identified for deletion | 50 |
protected-count |
Tags protected from deletion | 10 |
summary |
JSON summary of the operation | {...} |
The following tags are always protected from deletion:
latestmain,master,develop- Semantic version tags:
v1.0.0,1.0.0,1.0,1 - Any tags specified in
protected-tagsinput
| Pattern | Example | Default Retention |
|---|---|---|
| PR tags | pr-123, pr-456 |
30 days |
| Branch SHA tags | main-abc123f, develop-xyz789a |
14 days |
Define custom patterns with specific retention periods:
custom-patterns: |
{
"^feature-.*": 7,
"^hotfix-.*": 3,
"^release-.*": 60,
"^test-.*": 1
}name: Weekly Docker Cleanup
on:
schedule:
- cron: '0 2 * * 0' # Every Sunday at 2 AM UTC
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Clean Docker Hub
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'my-app'
pr-retention-days: 30
sha-retention-days: 7
dry-run: falsename: Manual Docker Cleanup
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run mode'
required: true
default: 'true'
type: choice
options:
- 'true'
- 'false'
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Clean Docker Hub
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'app1,app2,app3'
dry-run: ${{ github.event.inputs.dry_run }}
verbose: truename: Matrix Docker Cleanup
on:
schedule:
- cron: '0 2 * * 0'
jobs:
cleanup:
runs-on: ubuntu-latest
strategy:
matrix:
repository:
- name: frontend
pr-retention: 30
sha-retention: 14
- name: backend
pr-retention: 60
sha-retention: 30
- name: worker
pr-retention: 14
sha-retention: 7
steps:
- uses: actions/checkout@v4
- name: Clean ${{ matrix.repository.name }}
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: ${{ matrix.repository.name }}
pr-retention-days: ${{ matrix.repository.pr-retention }}
sha-retention-days: ${{ matrix.repository.sha-retention }}
dry-run: false- name: Clean Docker Hub
id: cleanup
uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'my-app'
dry-run: false
- name: Process Results
run: |
echo "Deleted ${{ steps.cleanup.outputs.deleted-count }} tags"
echo "Protected ${{ steps.cleanup.outputs.protected-count }} tags"
# Parse JSON summary
echo '${{ steps.cleanup.outputs.summary }}' | jq '.'
- name: Send Notification
if: steps.cleanup.outputs.deleted-count > 50
run: |
echo "Large cleanup performed: ${{ steps.cleanup.outputs.deleted-count }} tags deleted"Always test with dry-run mode first:
- uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'test-repo'
dry-run: true # Preview only
verbose: true # Detailed outputTest the cleanup script locally:
# Clone the action repository
git clone https://github.com/lostlink/docker-cleanup.git
cd docker-cleanup
# Set environment variables
export DOCKERHUB_USERNAME=your-username
export DOCKERHUB_PASSWORD=your-password-or-token
export DOCKER_NAMESPACE=your-org # Optional: default namespace for unqualified repos
# Run with dry-run
python scripts/dockerhub-cleanup.py \
--repositories repo1 repo2 \
--pr-retention 30 \
--sha-retention 14 \
--dry-run \
--verbose
# Or specify repositories with their namespace
python scripts/dockerhub-cleanup.py \
--repositories myorg/repo1 anotherorg/repo2 personal-repo \
--dry-run \
--verbose- Verify your Docker Hub credentials are correct
- Ensure the Personal Access Token has write permissions
- Check that the username matches your Docker Hub account exactly
- Verify the repository names are correct
- Check the organization/namespace setting
- For repositories in other organizations, use the full
namespace/repositoryformat - Ensure repositories exist and are accessible with your credentials
- Verify tags meet deletion criteria (age and pattern)
- Check if running in dry-run mode
- Review retention period settings
- Check verbose logs for details
- The action implements exponential backoff for rate limits
- Consider running cleanup during off-peak hours
- Reduce frequency if hitting limits regularly
Enable verbose logging for detailed information:
- uses: lostlink/docker-cleanup@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repositories: 'my-app'
verbose: true # Enable debug output
dry-run: true # Safe testing- Execution Time: ~2-5 minutes per repository (depending on tag count)
- API Calls: ~10-50 per repository
- Rate Limits: Handles Docker Hub rate limits with automatic retry
- Storage Savings: Typically 50-80% reduction in stored tags
- Credentials are never logged or exposed
- Supports Docker Hub Personal Access Tokens
- Uses GitHub Secrets for secure storage
- Implements secure authentication with Docker Registry v2
- All operations use HTTPS
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
If you encounter any problems, please file an issue along with a detailed description.
- Docker Hub API documentation
- GitHub Actions community
- Contributors and users of this action
- Support for additional container registries (GitHub Container Registry, AWS ECR)
- Webhook notifications for cleanup summaries
- Advanced filtering options
- Cleanup based on image size
- Support for keeping N most recent tags
Made with ❤️ for the Docker community