Managing Running Container Lifetimes with the Docker Python SDK
By: Joseph D. Marhee
Tags: DevOps, Python, Container Orchestration, Docker, Automation

I began building a service recently that will, upon user request, create a container (to provide said service, not a Container-as-a-Service-style workload), and then after 1–3 hours, terminate the container (taking the user’s session and local data away with it). The process for this required that I:

Retrieve the container’s StartedAt timestamp Compare that timestamp to see if it’s been more than however long I’d like to limit a session to. If a container is over that limit, it is queued for being Stopped and then ultimate the image pruned (in a separate task)

The setup for the script looks something like:

from datetime import datetime

import docker, dateparser

and then initializing a string for comparison, normalized to look like the Docker API-provided timestamp (these get converted further down, this is just for readability in stdout and, while debugging, I can visually inspect the timestamps):

STARTING_TIME = str(datetime.now()).split(" ")[0]+ "T" + str(datetime.now()).split(" ")[1] + "Z"

then, before we do anything else, setup the Docker client:

client = docker.APIClient(base_url='unix://var/run/docker.sock')

In the SDK, stopping a container requires the use of the Low level API functionality, so unless you require these features, you can leave the client set to docker.from_env() without any further arguments.

def compare_time(run_time,container_started_at): dt = dateparser.parse(container_started_at).timestamp() mt = dateparser.parse(run_time).timestamp() interval = mt - dt return interval

This function will take two arguments: the STARTING_TIME above (when the script started running) and the timestamp the container was created, and converts them to seconds, and returns that time in seconds.

def eval_interval(interval): eval = (interval / 60 ) / 60 if eval > 2.0: return True else: return False

then we are checking if that difference in time exceeds, in this case, 2 hours. If it does, it returns True , which in the next function, indicates a running container is safe to terminate:

def check_containers(): to_delete = [] for c in client.containers.list(): start_at = c.attrs['State']['StartedAt'] interval = compare_time(STARTING_TIME,start_at) deleteable = eval_interval(interval) print(c.name + " (" + c.id + ") " + str(c.image) + " " + str(deleteable)) if deleteable == True: to_delete.append({"id": c.id, "name": c.name}) else: continue return to_delete

The to_delete list this returns is a list of dictionaries that contains the id and name of the containers it detected a deletable value of True , with that in hand, it is handed to a function to delete, or handle:

def delete_containers(to_delete): failed_deletions = [] for c in to_delete: try: running_container = str(c['id']) print("Deleting: " + running_container) print(client.stop(container=running_container)) except: failed_deletions.append(c) print("Failed to delete: " + c.id)

continue return failed_deletions

This ingests the list of deletable containers, and then creates its own list of new containers that could not be deleted (and then optionally, I can handle this however I want in retry behavior later, but for now, it just throws an exception before continuing).

All of this is executed via a main function:

def main(): to_delete = check_containers() delete_expired = delete_containers(to_delete) return delete_expired if __name__ == "__main__": main()

For more about the packages I used: