docker-image-cleanup 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: docker-image-cleanup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Clean up old Docker images from local storage with retention controls
|
|
5
|
+
Keywords: docker,cleanup,images,disk-space
|
|
6
|
+
Author: Michael Bianco
|
|
7
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
+
Requires-Dist: click>=8.3.0
|
|
9
|
+
Requires-Dist: docker>=7.1.0
|
|
10
|
+
Requires-Dist: structlog-config==0.6.0
|
|
11
|
+
Requires-Dist: whenever>=0.9.3
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Project-URL: Repository, https://github.com/iloveitaly/docker-image-cleanup
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Clean Up Images from Self-Hosted Docker Registries
|
|
17
|
+
|
|
18
|
+
If you're running a self-hosted Docker registry (using tools like [unregistry](https://github.com/psviderski/unregistry)), you'll want to clean up old images to reclaim disk space. This tool connects to your local Docker daemon and removes old images from specified repositories while keeping the ones you actually need.
|
|
19
|
+
|
|
20
|
+
The cleanup logic is simple: keep your N most recent images and anything built in the last M days. Everything else gets removed. Images currently in use by containers are automatically skipped.
|
|
21
|
+
|
|
22
|
+
This tool is designed for managing images from self-hosted registries and probably shouldn't be used for general local Docker cleanup.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install docker-image-cleanup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or using `uv`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install docker-image-cleanup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
The basic command takes one or more image repositories:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docker-image-cleanup myrepo/myimage
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can clean multiple repositories at once:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
docker-image-cleanup myrepo/image1 myrepo/image2
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Before running the cleanup for real, use `--dry-run` to see what would be removed:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
docker-image-cleanup --dry-run myrepo/myimage
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Adjust the retention policy with `--num-recent` and `--min-age-days`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Keep only the 3 most recent images
|
|
60
|
+
docker-image-cleanup --num-recent 3 myrepo/myimage
|
|
61
|
+
|
|
62
|
+
# Keep images from the last 7 days
|
|
63
|
+
docker-image-cleanup --min-age-days 7 myrepo/myimage
|
|
64
|
+
|
|
65
|
+
# Combine both options
|
|
66
|
+
docker-image-cleanup --num-recent 3 --min-age-days 7 myrepo/myimage
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- Removes old Docker images based on age and recency criteria
|
|
72
|
+
- Keeps a configurable number of recent images (default: 5)
|
|
73
|
+
- Preserves images newer than a specified age threshold (default: 30 days)
|
|
74
|
+
- Automatically skips images currently in use by containers
|
|
75
|
+
- Dry-run mode to preview changes before execution
|
|
76
|
+
- Human-readable disk space savings reporting
|
|
77
|
+
- Structured logging for detailed operation visibility
|
|
78
|
+
|
|
79
|
+
## How It Works
|
|
80
|
+
|
|
81
|
+
The cleanup process is straightforward:
|
|
82
|
+
|
|
83
|
+
1. Lists all images for the specified repository
|
|
84
|
+
2. Identifies images to keep based on retention criteria (most recent N images and images newer than M days)
|
|
85
|
+
3. Removes or untags images that don't meet retention criteria
|
|
86
|
+
4. Skips any images currently in use by containers
|
|
87
|
+
5. Reports total disk space saved
|
|
88
|
+
|
|
89
|
+
Images with multiple tags are handled intelligently. If all tags on an image would be removed, the entire image is deleted. If only some tags would be removed, those tags are untagged but the image remains.
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python 3.11+
|
|
94
|
+
- Docker daemon running locally
|
|
95
|
+
- Docker Python SDK
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Clone the repository
|
|
101
|
+
git clone https://github.com/iloveitaly/docker-image-cleanup
|
|
102
|
+
cd docker-image-cleanup
|
|
103
|
+
|
|
104
|
+
# Install dependencies
|
|
105
|
+
uv sync
|
|
106
|
+
|
|
107
|
+
# Run tests
|
|
108
|
+
pytest
|
|
109
|
+
|
|
110
|
+
# Run the CLI
|
|
111
|
+
uv run docker-image-cleanup --help
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
# [MIT License](LICENSE.md)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Clean Up Images from Self-Hosted Docker Registries
|
|
2
|
+
|
|
3
|
+
If you're running a self-hosted Docker registry (using tools like [unregistry](https://github.com/psviderski/unregistry)), you'll want to clean up old images to reclaim disk space. This tool connects to your local Docker daemon and removes old images from specified repositories while keeping the ones you actually need.
|
|
4
|
+
|
|
5
|
+
The cleanup logic is simple: keep your N most recent images and anything built in the last M days. Everything else gets removed. Images currently in use by containers are automatically skipped.
|
|
6
|
+
|
|
7
|
+
This tool is designed for managing images from self-hosted registries and probably shouldn't be used for general local Docker cleanup.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install docker-image-cleanup
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or using `uv`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install docker-image-cleanup
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The basic command takes one or more image repositories:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker-image-cleanup myrepo/myimage
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can clean multiple repositories at once:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker-image-cleanup myrepo/image1 myrepo/image2
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Before running the cleanup for real, use `--dry-run` to see what would be removed:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
docker-image-cleanup --dry-run myrepo/myimage
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Adjust the retention policy with `--num-recent` and `--min-age-days`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Keep only the 3 most recent images
|
|
45
|
+
docker-image-cleanup --num-recent 3 myrepo/myimage
|
|
46
|
+
|
|
47
|
+
# Keep images from the last 7 days
|
|
48
|
+
docker-image-cleanup --min-age-days 7 myrepo/myimage
|
|
49
|
+
|
|
50
|
+
# Combine both options
|
|
51
|
+
docker-image-cleanup --num-recent 3 --min-age-days 7 myrepo/myimage
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- Removes old Docker images based on age and recency criteria
|
|
57
|
+
- Keeps a configurable number of recent images (default: 5)
|
|
58
|
+
- Preserves images newer than a specified age threshold (default: 30 days)
|
|
59
|
+
- Automatically skips images currently in use by containers
|
|
60
|
+
- Dry-run mode to preview changes before execution
|
|
61
|
+
- Human-readable disk space savings reporting
|
|
62
|
+
- Structured logging for detailed operation visibility
|
|
63
|
+
|
|
64
|
+
## How It Works
|
|
65
|
+
|
|
66
|
+
The cleanup process is straightforward:
|
|
67
|
+
|
|
68
|
+
1. Lists all images for the specified repository
|
|
69
|
+
2. Identifies images to keep based on retention criteria (most recent N images and images newer than M days)
|
|
70
|
+
3. Removes or untags images that don't meet retention criteria
|
|
71
|
+
4. Skips any images currently in use by containers
|
|
72
|
+
5. Reports total disk space saved
|
|
73
|
+
|
|
74
|
+
Images with multiple tags are handled intelligently. If all tags on an image would be removed, the entire image is deleted. If only some tags would be removed, those tags are untagged but the image remains.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Python 3.11+
|
|
79
|
+
- Docker daemon running locally
|
|
80
|
+
- Docker Python SDK
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Clone the repository
|
|
86
|
+
git clone https://github.com/iloveitaly/docker-image-cleanup
|
|
87
|
+
cd docker-image-cleanup
|
|
88
|
+
|
|
89
|
+
# Install dependencies
|
|
90
|
+
uv sync
|
|
91
|
+
|
|
92
|
+
# Run tests
|
|
93
|
+
pytest
|
|
94
|
+
|
|
95
|
+
# Run the CLI
|
|
96
|
+
uv run docker-image-cleanup --help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
# [MIT License](LICENSE.md)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import docker
|
|
3
|
+
import docker.errors
|
|
4
|
+
import structlog
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from whenever import Instant
|
|
7
|
+
from structlog_config import configure_logger
|
|
8
|
+
|
|
9
|
+
configure_logger()
|
|
10
|
+
log = structlog.get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def human_size(size_bytes):
|
|
14
|
+
size = size_bytes
|
|
15
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
16
|
+
if size < 1024:
|
|
17
|
+
return f"{size:.2f} {unit}"
|
|
18
|
+
size /= 1024
|
|
19
|
+
return f"{size:.2f} PB"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def clean_repo(client, image_repo, num_recent, min_age_days, dry_run, now):
|
|
23
|
+
images = client.images.list(name=image_repo)
|
|
24
|
+
if not images:
|
|
25
|
+
log.info("no images", repo=image_repo)
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
sorted_images = sorted(
|
|
29
|
+
images, key=lambda img: Instant.parse_iso(img.attrs["Created"]), reverse=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
keep_tags = set()
|
|
33
|
+
for img in sorted_images[:num_recent]:
|
|
34
|
+
keep_tags.update(img.tags)
|
|
35
|
+
|
|
36
|
+
min_age = now.subtract(hours=min_age_days * 24)
|
|
37
|
+
for img in images:
|
|
38
|
+
created = Instant.parse_iso(img.attrs["Created"])
|
|
39
|
+
log.debug("inspecting image", image_id=img.id, tags=img.tags, created=created)
|
|
40
|
+
if created >= min_age:
|
|
41
|
+
keep_tags.update(img.tags)
|
|
42
|
+
|
|
43
|
+
tags_to_remove = []
|
|
44
|
+
for img in images:
|
|
45
|
+
for tag in img.tags:
|
|
46
|
+
if tag not in keep_tags and tag.startswith(image_repo + ":"):
|
|
47
|
+
tags_to_remove.append((tag, img))
|
|
48
|
+
|
|
49
|
+
remove_by_id = defaultdict(list)
|
|
50
|
+
for tag, img in tags_to_remove:
|
|
51
|
+
remove_by_id[img.id].append(tag)
|
|
52
|
+
|
|
53
|
+
total_saved = 0
|
|
54
|
+
for img_id, remove_tags in remove_by_id.items():
|
|
55
|
+
img = client.images.get(img_id)
|
|
56
|
+
all_tags = img.tags
|
|
57
|
+
if set(remove_tags) == set(all_tags):
|
|
58
|
+
in_use = (
|
|
59
|
+
len(client.containers.list(all=True, filters={"ancestor": img_id})) > 0
|
|
60
|
+
)
|
|
61
|
+
if in_use:
|
|
62
|
+
log.warning("skipped image in use", image_id=img_id, tags=remove_tags)
|
|
63
|
+
continue
|
|
64
|
+
size = img.attrs.get("Size", 0)
|
|
65
|
+
if dry_run:
|
|
66
|
+
log.info(
|
|
67
|
+
"would remove image",
|
|
68
|
+
image_id=img_id,
|
|
69
|
+
tags=remove_tags,
|
|
70
|
+
size=human_size(size),
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
for tag in remove_tags:
|
|
74
|
+
client.images.remove(tag, force=False)
|
|
75
|
+
log.info(
|
|
76
|
+
"removed image",
|
|
77
|
+
image_id=img_id,
|
|
78
|
+
tags=remove_tags,
|
|
79
|
+
size=human_size(size),
|
|
80
|
+
)
|
|
81
|
+
total_saved += size
|
|
82
|
+
else:
|
|
83
|
+
if dry_run:
|
|
84
|
+
log.info("would untag", tags=remove_tags)
|
|
85
|
+
else:
|
|
86
|
+
for tag in remove_tags:
|
|
87
|
+
try:
|
|
88
|
+
client.images.remove(tag, force=False)
|
|
89
|
+
log.info("untagged", tag=tag)
|
|
90
|
+
except docker.errors.APIError as e:
|
|
91
|
+
log.warning("skipped untag", tag=tag, reason=str(e))
|
|
92
|
+
|
|
93
|
+
msg = "total space that would be saved" if dry_run else "total space saved"
|
|
94
|
+
log.info(msg, bytes=total_saved, human=human_size(total_saved))
|
|
95
|
+
return total_saved
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@click.command()
|
|
99
|
+
@click.argument("image_repos", nargs=-1, required=True)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--num-recent", default=5, help="Number of recent tags to keep.", show_default=True
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--min-age-days",
|
|
105
|
+
default=30,
|
|
106
|
+
help="Minimum age in days to keep tags.",
|
|
107
|
+
show_default=True,
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--dry-run",
|
|
111
|
+
is_flag=True,
|
|
112
|
+
help="Simulate removal without executing.",
|
|
113
|
+
show_default=True,
|
|
114
|
+
)
|
|
115
|
+
def main(image_repos, num_recent, min_age_days, dry_run):
|
|
116
|
+
client = docker.from_env()
|
|
117
|
+
now = Instant.now()
|
|
118
|
+
|
|
119
|
+
grand_total = 0
|
|
120
|
+
for image_repo in image_repos:
|
|
121
|
+
log.info("cleaning repo", repo=image_repo)
|
|
122
|
+
grand_total += clean_repo(
|
|
123
|
+
client, image_repo, num_recent, min_age_days, dry_run, now
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if len(image_repos) > 1:
|
|
127
|
+
msg = (
|
|
128
|
+
"grand total space that would be saved"
|
|
129
|
+
if dry_run
|
|
130
|
+
else "grand total space saved"
|
|
131
|
+
)
|
|
132
|
+
log.info(msg, bytes=grand_total, human=human_size(grand_total))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docker-image-cleanup"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Clean up old Docker images from local storage with retention controls"
|
|
5
|
+
keywords = ["docker", "cleanup", "images", "disk-space"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"click>=8.3.0",
|
|
10
|
+
"docker>=7.1.0",
|
|
11
|
+
"structlog-config==0.6.0",
|
|
12
|
+
"whenever>=0.9.3",
|
|
13
|
+
]
|
|
14
|
+
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
15
|
+
urls = { "Repository" = "https://github.com/iloveitaly/docker-image-cleanup" }
|
|
16
|
+
|
|
17
|
+
# additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
|
18
|
+
[project.scripts]
|
|
19
|
+
docker-image-cleanup = "docker_image_cleanup:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.8.11,<0.9.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[tool.uv.build-backend]
|
|
26
|
+
# avoids the src/ directory structure
|
|
27
|
+
module-root = ""
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8.3.3","pyright>=1.1.380","ruff>=0.8.0"]
|
|
31
|
+
|