Skip to content

luthfialghz/portainer-image-cleanup

Repository files navigation

portainer-image-cleanup

Automated Docker image cleanup for Portainer — removes unused images older than 30 days via Portainer API, deployable as a Portainer Stack.


Features

  • Deletes Docker images unused for more than N days (default: 30)
  • Skips images actively used by running or stopped containers
  • Cleans up dangling images and build cache
  • Dry run mode — simulate without deleting anything
  • Fully configurable via environment variables
  • Deployable as a Portainer Stack (no host access required)
  • Persistent logs with automatic rotation
  • Timezone-aware scheduling via cron

Requirements

Requirement Version
Portainer CE 2.14+
Docker 20.10+

Project Structure

.
├── Dockerfile                    # Alpine-based image (bash + curl + jq)
├── entrypoint.sh                 # Container entrypoint — sets up and runs crond
├── cleanup_portainer_images.sh   # Main script — calls Portainer API
├── docker-compose.yml            # Portainer Stack file
├── .env.example                  # Configuration template
└── .gitignore

Getting Started

1. Get Portainer API Token

  1. Open Portainer → click your username on the bottom-left sidebar
  2. Go to My account → scroll down to Access Tokens
  3. Click Add access token → enter a description (e.g. image-cleanup) → confirm
  4. Copy the token immediately — it is only shown once

The token looks like:

ptr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2. Find your Endpoint ID

Open an environment in Portainer and look at the URL:

http://localhost:9000/#!/1/docker/containers
                          ^
                     this number is your ENDPOINT_ID

3. Find the Portainer Docker Network

Run this on your host to find the network name used by Portainer:

docker network ls | grep portainer

Update the networks.portainer-network.name value in docker-compose.yml to match.

4. Deploy as Portainer Stack

  1. Open Portainer → StacksAdd stack
  2. Name it image-cleanup
  3. Choose Upload → select docker-compose.yml
  4. Scroll down to Environment variables and fill in:
Variable Example Value Description
PORTAINER_URL http://portainer:9000 Portainer URL reachable from inside the container
PORTAINER_TOKEN ptr_xxxxx API token from Step 1
ENDPOINT_ID 1 Environment ID from Step 2
MAX_AGE_DAYS 30 Delete images older than N days
CRON_SCHEDULE 0 2 * * * Cron schedule (default: daily at 02:00)
DRY_RUN true Set true first to simulate
RUN_ON_START false Run cleanup immediately on container start
TZ Asia/Jakarta Container timezone
  1. Click Deploy the stack

Configuration Reference

Variable Default Description
PORTAINER_URL http://portainer:9000 Portainer base URL
PORTAINER_TOKEN (empty) API token — takes priority over username/password
PORTAINER_USERNAME admin Fallback if no token is set
PORTAINER_PASSWORD (empty) Fallback if no token is set
ENDPOINT_ID 1 Portainer environment/endpoint ID
MAX_AGE_DAYS 30 Minimum image age in days before deletion
CRON_SCHEDULE 0 2 * * * Standard cron expression
RUN_ON_START false Run once immediately when container starts
DRY_RUN false Simulate without deleting
KEEP_LOGS_DAYS 90 Days to retain log files
TZ Asia/Jakarta Timezone for cron scheduling

Cron Schedule Examples

Schedule Expression
Every day at 02:00 0 2 * * *
Every Sunday at 03:00 0 3 * * 0
Every Monday–Friday at 02:00 0 2 * * 1-5
Every 12 hours 0 */12 * * *

How It Works

Container starts
└── entrypoint.sh
      ├── Sets timezone
      ├── Exports env vars to /app/.env_runtime
      ├── Registers cron job with CRON_SCHEDULE
      ├── (optional) Runs cleanup immediately if RUN_ON_START=true
      └── Starts crond in foreground

On each cron trigger
└── cleanup_portainer_images.sh
      ├── Authenticates to Portainer API
      ├── Fetches all images from the endpoint
      ├── Fetches all containers (to detect image usage)
      ├── For each image:
      │     ├── SKIP  — if age < MAX_AGE_DAYS
      │     ├── SKIP  — if used by any container
      │     └── DELETE — via DELETE /api/endpoints/{id}/docker/images/{id}
      ├── Prunes dangling images
      ├── Clears build cache
      ├── Rotates old logs
      └── Prints summary

Viewing Logs

In Portainer, open the image-cleanup container and click Logs.

Or from the host:

docker logs portainer-image-cleanup
docker logs portainer-image-cleanup --follow

Log files are also stored in the cleanup-logs Docker volume:

docker run --rm -v portainer-image-cleanup_cleanup-logs:/logs alpine ls /logs

Running Locally (without Portainer Stack)

# Clone and configure
cp .env.example .env
# Edit .env with your values

# Dry run (simulate)
docker compose run --rm -e DRY_RUN=true image-cleanup

# Live run
docker compose up -d

Security Notes

  • Never commit .env — it is listed in .gitignore
  • Use API Token instead of username/password when possible
  • The container does not require access to the Docker socket on the host
  • All communication goes through the Portainer API over HTTP/HTTPS

License

MIT

About

Automated Docker image cleanup for Portainer — removes unused images older than 30 days via Portainer API, deployable as a Portainer Stack.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors