🚀 Executive Summary

TL;DR: Old, dangling Docker images frequently consume disk space on CI/CD runners, causing ‘disk full’ alerts and requiring manual intervention. This guide provides a Python script to automate the removal of such images based on age or dangling status, scheduled via cron, to maintain lean infrastructure.

🎯 Key Takeaways

  • Utilize the `docker` Python SDK, specifically `docker.from_env()`, to programmatically interact with the Docker daemon for image management.
  • Identify images for removal by checking if they are ‘dangling’ (no tags) or older than a configurable `RETENTION_DAYS` using `image.attrs.get(‘Created’)`.
  • Implement robust error handling with `try…except docker.errors.APIError` (status code 409) during `client.images.remove(image_id, force=True)` to safely skip images in use by containers or parent images.

Automate removing old Docker Images to free up Disk Space

Automate removing old Docker Images to free up Disk Space

Hey team, Darian here. I wanted to share a quick automation trick that’s saved me countless hours and prevented quite a few “disk full” alerts on our CI/CD runners. Our build servers used to grind to a halt every few weeks, disk space completely eaten by old, dangling Docker images. I was the one stuck SSH-ing in and running cleanup commands manually. After one too many late-night alerts, I wrote this simple Python script to handle it for me. Now, it’s a ‘set it and forget it’ task that keeps our infrastructure lean.

This guide will walk you through creating and scheduling that exact script.

Prerequisites

  • A server (or local machine) with Docker installed and running.
  • Python 3.6 or newer installed.
  • Permissions to interact with the Docker daemon.

The Guide: Step-by-Step

Step 1: Setting up your Python Environment

First things first, let’s get our project environment ready. I’ll skip the standard virtualenv setup commands since you likely have your own workflow for that. Just make sure you’re in a clean project directory with an active Python virtual environment.

The only external library we need is the official Docker SDK for Python. You can install it using pip. Just run this command in your terminal:

pip install docker

With that installed, we’re ready to write the logic.

Step 2: The Python Cleanup Script

Create a new file named cleanup_docker.py. The script will connect to the Docker daemon, list all images, and then selectively remove any that are either “dangling” (not tagged and not used by any container) or older than a specific number of days.

Here is the full script. I’ll break down the logic right below it.


import docker
from datetime import datetime, timedelta, timezone

# --- Configuration ---
# How many days should we keep images? Images older than this will be removed.
RETENTION_DAYS = 30

def main():
    print("Starting Docker image cleanup...")
    
    try:
        client = docker.from_env()
        # Test the connection
        client.ping()
    except Exception as e:
        print(f"Error connecting to Docker daemon: {e}")
        return # Exit the function if we can't connect

    # Get the current time in UTC to compare against image creation times
    now = datetime.now(timezone.utc)
    cutoff_date = now - timedelta(days=RETENTION_DAYS)
    
    print(f"Removing images created before: {cutoff_date.isoformat()}")

    images_to_remove = []
    
    try:
        all_images = client.images.list(all=True)
    except Exception as e:
        print(f"Error listing Docker images: {e}")
        return

    for image in all_images:
        # Dangling images have no tags
        is_dangling = not image.tags
        
        # Docker API provides creation date as an ISO 8601 string with nanoseconds,
        # which Python's fromisoformat can't parse directly. We trim it.
        created_str = image.attrs.get('Created', '')
        if not created_str:
            continue
        
        # Truncate to microseconds and add 'Z' for UTC if missing
        if '.' in created_str:
            created_str = created_str.split('.')[0] + 'Z'
        
        try:
            created_dt = datetime.fromisoformat(created_str.replace('Z', '+00:00'))
        except ValueError:
            print(f"Warning: Could not parse date for image {image.id[:12]}: {created_str}")
            continue

        is_old = created_dt < cutoff_date
        
        if is_dangling or is_old:
            # We add the short ID for logging purposes
            images_to_remove.append((image.id, image.tags))

    if not images_to_remove:
        print("No old or dangling images to remove.")
        return

    print(f"Found {len(images_to_remove)} images to potentially remove.")
    
    removed_count = 0
    for image_id, tags in images_to_remove:
        try:
            # The 'force=True' flag is similar to 'docker rmi -f'.
            # It will remove the image even if it's tagged in multiple repos.
            # It will NOT remove images that are in use by a container.
            client.images.remove(image_id, force=True)
            tag_info = f"with tags {tags}" if tags else "(dangling)"
            print(f"Successfully removed image {image_id[:12]} {tag_info}")
            removed_count += 1
        except docker.errors.APIError as e:
            # This typically happens if the image is a parent of another image
            # or is actively being used by a container. We can safely ignore these.
            if e.response.status_code == 409: # Conflict error
                print(f"Could not remove image {image_id[:12]}: it is in use by a container or is a parent image.")
            else:
                print(f"API Error removing image {image_id[:12]}: {e}")
        except Exception as e:
            print(f"An unexpected error occurred while removing image {image_id[:12]}: {e}")

    print(f"\nCleanup complete. Removed {removed_count} images.")

if __name__ == "__main__":
    main()

Breaking down the logic:

  1. Configuration: I’ve set a RETENTION_DAYS variable at the top. This makes it easy to adjust how aggressive the cleanup is. 30 days is a safe starting point.
  2. Connect to Docker: docker.from_env() is the magic here. It automatically finds and connects to your Docker daemon, just like the `docker` command-line tool does.
  3. Calculate Cutoff Date: We get the current time and subtract our retention period. Any image created before this cutoff_date is a candidate for deletion.
  4. Iterate and Identify: The script fetches all images and checks two conditions: Is it dangling (not image.tags)? Or is it older than our cutoff date? If either is true, it’s added to our removal list.
  5. Safe Removal: We loop through the identified images and attempt to remove them. The crucial part is the try...except block. If you try to remove an image that a running container is using, the Docker API will throw an error. Our script catches this “Conflict” error (status code 409) and simply prints a message, ensuring we never accidentally break a running application.

Pro Tip: In my production setups, I don’t hardcode the RETENTION_DAYS. Instead, I pull it from an environment variable or a simple config file. This lets me change the retention policy without modifying the script’s code, which is great for CI/CD pipelines.

Step 3: Automating with a Cron Job

A script is only useful if you don’t have to remember to run it. On any Linux-based system, `cron` is the perfect tool for the job.

You’ll need to edit your user’s crontab file. You can typically do this by running a command to open the editor. Inside, you’ll add a line that specifies the schedule and the command to run.

This line will run our script at 2:00 AM every Monday:


0 2 * * 1 python3 cleanup_docker.py >> /home/darian/docker_cleanup.log 2>&1

Let’s unpack that:

  • 0 2 * * 1: The schedule. It means at minute 0, hour 2, on any day of the month, any month, but only on the first day of the week (Monday).
  • python3 cleanup_docker.py: The command to execute. Make sure to use the full path to your script if it’s not in a directory included in your system’s PATH.
  • >> /home/darian/docker_cleanup.log 2>&1: This is for logging. It appends all output (both standard output and errors) to a log file. I strongly recommend this so you can check in and see what the script has been doing.

Here’s Where I Usually Mess Up (Common Pitfalls)

  • Permissions, Permissions, Permissions: The number one issue is the script failing because it can’t connect to the Docker socket. Your user needs to be in the docker group to manage Docker without `sudo`. If you see a “Permission Denied” error, this is almost always the cause.
  • Setting Retention Too Low: Be careful with that RETENTION_DAYS variable. I once set it to 1 day during testing on a shared server and accidentally wiped some base images a teammate was actively using for a feature branch. Start with a conservative value like 30 or 60 days.
  • Cron Job Paths: Cron jobs run with a very minimal environment. Always use full paths to your Python executable and your script unless you are certain the paths are configured correctly in the cron environment. The example above assumes `python3` is in the path and the cron job is set relative to the script’s location. A more robust cron command would use full paths for both.

Conclusion

And that’s it. With one Python script and a single line in your crontab, you’ve automated a tedious but critical maintenance task. This frees up valuable disk space on your build agents and servers, preventing performance degradation and outright failures. It’s a small piece of automation that delivers consistent, reliable value. Hope this helps you out!

– Darian Vance

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How can I automatically manage and clean up old Docker images to free disk space?

You can automate Docker image cleanup using a Python script with the Docker SDK to identify and remove images that are dangling or older than a specified `RETENTION_DAYS`, then schedule this script with a cron job.

âť“ How does this Python script-based cleanup compare to using `docker system prune`?

While `docker system prune` offers general cleanup, this Python script provides more granular control, allowing for specific age-based retention policies (e.g., 30 days) and custom logic that `docker system prune` does not directly support for image age.

âť“ What are common issues encountered when setting up this Docker image cleanup automation?

Common pitfalls include `Permission Denied` errors if the user is not in the `docker` group, accidentally setting `RETENTION_DAYS` too low and deleting active base images, and cron job failures due to incorrect or non-absolute paths for the Python executable or script.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading