docker-image-cleanup 0.1.0__py3-none-any.whl

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,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,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,5 @@
1
+ docker_image_cleanup/__init__.py,sha256=L_OWIaHxnWW-TpVPq3k8KqMY73ezmwPN0DE7ZZ45GNI,4154
2
+ docker_image_cleanup-0.1.0.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
3
+ docker_image_cleanup-0.1.0.dist-info/entry_points.txt,sha256=YEaDaNegEnziYGJGU9iU5uLWrQ72_8YdQJCPDeKFWXo,68
4
+ docker_image_cleanup-0.1.0.dist-info/METADATA,sha256=bvwgft_uM54_ECYZ-v_Q6YMWyOHwN1XcVCS5A8oyB9o,3429
5
+ docker_image_cleanup-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ docker-image-cleanup = docker_image_cleanup:main
3
+