# aiodocker wraps the Docker HTTP API in asyncio, with a dependency count of one

> aiodocker is a Python client for the Docker Engine API built on aiohttp and asyncio, covering images, containers, volumes, networks, exec, swarm and events. It is a thin wrapper rather than a daemon, it needs a Docker socket to talk to, and its own dependency pin on aiohttp carries an explicit upper bound because of a vendored queue subclass.

**aio-libs/aiodocker** — Python Docker API client based on asyncio and aiohttp

- Repository: https://github.com/aio-libs/aiodocker
- Stars: 538 · Forks: 118
- Language: Python
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/aio-libs-aiodocker

## What it is: an HTTP client for the Docker Engine API, in async form

The README's own one-line description is the shortest accurate summary available: a simple Docker HTTP API wrapper written with asyncio and aiohttp. The title of the project is AsyncIO bindings for docker.io.

That framing sets the expectation correctly. aiodocker is not a Docker management layer, not a panel, and not a queue. It is a Python client for the Engine API that happens to be written in the async style, so that a program already running an event loop can talk to Docker without a thread pool and without blocking on every container operation.

The API surface follows the Docker resource model. The example in the README walks the two operations you would write first: listing images through docker.images.list() and listing containers through docker.containers.list(). Each returned object is a plain dict rather than a typed model, since the example indexes image['Id'] and image['RepoTags'] directly.

The repository metadata gives the topics as aiohttp, api-client, asyncio, docker and python, which is an honest description of a library whose job is to translate HTTP into awaitables. The primary language is Python and the licence is Apache-2.0, recorded in the project metadata as a text field and with a LICENSE file at the repository root.

The client is created and closed explicitly. The example ends with await docker.close(), and the Docker object is constructed with no arguments, so it talks to the local Docker daemon by default. The connection is a resource with a lifetime, and the example treats it that way.

The examples directory in the repository shows the range: events.py for the event stream, info.py for daemon information, ssh_example.py and ssh_example_advanced.py for the SSH transport, and stdio_stdout.py for attaching to a container's streams. Those five files are a better index of the library's coverage than the feature list, because they are working code.

## The dependency pin tells you more than the version number does

The runtime dependency list in pyproject.toml contains one package, aiohttp>=3.11.13,<4.0, and the comment above it is the most detailed dependency note you will find in most Python libraries.

The lower bound has a specific reason: aiohttp 3.11.13 backported the fix for a break inside a finally block in web_protocol.py, referenced as aio-libs/aiohttp#10434, which Python 3.14 turns into a SyntaxWarning under PEP 765. The comment also notes that older aiohttp versions are end of life.

The upper bound is described as interim. The vendored FlowControlDataQueue still subclasses aiohttp.DataQueue, whose private surface may change in aiohttp 4.0, and the comment points to a tracking issue on the aiodocker repository.

That is the interesting part of the whole design, and it is worth reading as a maintainer rather than as a user. The library vendors part of aiohttp's internals, which buys it control over flow control, and that decision is what forces the upper bound. When a dependency's internals are used, the dependency's major version becomes your problem, and the honest way to handle that is to say so in the metadata and track it in an issue rather than discovering it at upgrade time.

The optional extras are grouped by purpose. The ssh extra pulls asyncssh>=2.14.0 and paramiko>=2.9.0, and it is the only one that changes the transport rather than the tooling. The dev extra pins aiohttp==3.13.5 alongside towncrier==25.8.0, the changelog tool. The lint extra is pre-commit>=3.5.0 with ruff==0.15.14 and mypy==2.1.0 plus types-paramiko, so the project lints and type checks with pre-commit running the hooks. The test extra is the heaviest, with pytest==9.0.3, pytest-asyncio==1.4.0, pytest-cov==7.1.0, pytest-sugar==1.1.1, codecov==2.1.13, packaging==26.2, async-timeout~=5.0.1 and testcontainers==4.14.2, and the doc extra is sphinx==8.1.3 with alabaster==1.0.0.

The presence of testcontainers in the test dependencies is the one to notice, because it changes what the test suite means. The tests are not mocking the Docker daemon, they are starting real containers.

## Installing, and the difference between a user and a contributor

For a user of the library the installation is one command:

```sh
   pip install aiodocker
```

For a contributor, the README recommends uv, and gives the reason: it manages the virtualenv and resolves dependencies from the committed uv.lock so every contributor and CI job builds against the same versions.

```sh
   # Install uv first: https://docs.astral.sh/uv/getting-started/installation/
   uv sync --extra dev --extra lint --extra test --extra doc
   uv run pre-commit install
```

The README then states that the Makefile helpers, make develop, make install, make lint and make test, all assume uv. That is the shape of a project that has standardised on one tool for everything, and the Makefile is a thin layer over it rather than a parallel path.

There is a documented pip fallback for anyone who cannot install uv, and the README is candid about the cost:

```sh
   python -m venv .venv && source .venv/bin/activate
   pip install -U pip
   pip install -e '.[dev,lint,test,doc]'  # in zsh, you need to escape brackets
   pre-commit install
```

The resulting environment is not pinned to uv.lock, so versions may drift from CI. The zsh note about escaping the brackets is the kind of detail that saves a confused afternoon, since unquoted square brackets are a glob pattern in the shell.

Running the tests is either the Makefile target or a direct pytest invocation:

```sh
   # Run all tests
   make test

   # Run individual tests
   uv run pytest tests/test_images.py
```

Building the distributions is uv build, and the README notes that you usually do not need to run this step yourself.

For a library with a single runtime dependency, the contributor setup is the heavier part, and that is normal. The lockfile, the extras split into dev, lint, test and doc, the pre-commit hooks and the pinned tool versions are what make a small library reviewable by more than one person.

## How the Docker API surfaces in an async client

The example code is the clearest documentation of the shape, and it is worth reading closely because it shows the conventions the library uses.

Creating a container is a single call with a config dict in the Docker API's own format, and the method name is create_or_replace:

```python
    async def run_container(docker):
        print('== Running a hello-world container ==')
        container = await docker.containers.create_or_replace(
            config={
                'Cmd': ['/bin/ash', '-c', 'echo "hello world"'],
                'Image': 'alpine:latest',
            },
            name='testing',
        )
        await container.start()
        logs = await container.log(stdout=True)
        print(''.join(logs))
        await container.delete(force=True)
```

The config dict uses the Docker API's own keys, Cmd, Image, name, with the image tag set to alpine:latest. The method name create_or_replace tells you the semantics: it is idempotent with respect to a container of that name, which is the behaviour you want in a service that restarts.

Logs are read with await container.log(stdout=True) and come back as something you join, print(''.join(logs)), so the API returns an iterable of chunks rather than a single string. That matters for a long running container, because a single call that returns everything is not a streaming interface.

Deletion is await container.delete(force=True). The force flag is in the Docker API and is passed straight through.

Two conventions are visible throughout. Resource methods are awaitable, and the Docker API's snake_case names are kept as they are on the wire, so a config dict can be copied from Docker's own documentation without translation. The one exception is the container identifier, which the example reads as container._id with a leading underscore, because id is a Python builtin.

The main entry point is small:

```python
    async def main():
        docker = aiodocker.Docker()
        await list_things(docker)
        await run_container(docker)
        await docker.close()
```

No arguments to the constructor, no session object to manage, and one close call at the end. The library has no configuration surface of its own, which is consistent with it being a wrapper rather than a framework.

## Where aiodocker is the wrong tool

The first limitation is the transport, and it is a security decision rather than a functional one. aiodocker talks to the Docker daemon, and on Linux that means the Docker socket, which is root-equivalent access to the host. The library does nothing about that, because the transport is the caller's problem, and any process that can reach the socket can start a container with the host's filesystem mounted.

That is why the SSH extras exist at all. The ssh extra with asyncssh and paramiko lets the client reach a remote Docker daemon over an encrypted channel instead of a local socket, which changes the exposure from a local root-equivalent socket to a network service with credentials. The two example files, ssh_example.py and ssh_example_advanced.py, exist because this is a real deployment mode rather than a hypothetical.

The second limitation is scope. This is a client for the Docker Engine API, and the Engine API is a moving target with a large surface. There is no registry API, no Swarm mode coverage guarantee, no buildx support described anywhere in the README, and no orchestration. If you need to manage a cluster, an image registry or a CI pipeline of builds, you need something else.

The third limitation is the type story. The example indexes image['Id'] and container._id directly on plain dicts, so you get the Docker API's payload shape rather than typed models. That is fast and faithful, and it also means a change in the upstream API's field names surfaces as a KeyError in your code rather than as a type error at the boundary.

The alternative to weigh is docker-py, the synchronous Docker SDK for Python, or the Docker CLI wrapped in a subprocess. The difference in approach is exactly the one the project name advertises. docker-py is mature and synchronous, which makes it the right choice for a script that does a handful of container operations and exits, because blocking calls are simpler there. aiodocker is the right choice when the container work is one part of a service that is already asynchronous, because a blocking HTTP call inside an event loop stalls every other coroutine in the process, and the usual answer to that is a thread executor around a synchronous client, which is more code than using the async client directly.

There is also the option of not using the Docker API at all for simple cases. Running a container and streaming its output is a shell command, and if that is all you need, the wrapper is an extra layer.

## The SSH transport, the events stream and the project tooling

Two features go beyond the basic resource CRUD, and both are the reason to use a wrapper rather than raw HTTP.

The first is the events stream, which examples/events.py demonstrates. The Docker Engine exposes an event stream, and consuming it in an async client is natural: you await the next event rather than polling for changes. The library's own README lists events among the areas it covers, and the dedicated example file is the reference for how to subscribe.

The second is the SSH transport. Because aiodocker can talk to a remote daemon, the same code that manages a local container can manage one on another host. The ssh extra installs asyncssh and paramiko, and the examples show both a simple case and an advanced one, which suggests the advanced file covers the options around authentication or connection handling that a first example does not.

The repository tooling is where you can see how seriously the project takes review. The Makefile targets are thin wrappers over uv, and lint has a CI branch that adds --show-diff-on-failure to the pre-commit run, so a failing hook in CI prints the diff that caused it rather than just the filename. The same target then runs mypy afterwards. There is a create-tar target that builds tests/docker/docker_context.tar from tests/docker/tar/, which is a test fixture packaged as an archive for the container tests to load.

The test target runs bash test.sh rather than pytest directly, so the shell script is where the actual orchestration lives, and the individual file can still be run on its own with uv run pytest tests/test_images.py. The docs target builds Sphinx HTML with -W -E, meaning warnings are errors and the environment is rebuilt fresh, so a stale build cannot hide a broken reference.

The top level entries show the rest of the shape: a docs/ directory, a CHANGES.rst alongside a CHANGES/ directory with towncrier managing the fragments, CONTRIBUTORS.txt, a codecov.yml, a .readthedocs.yaml, a .pre-commit-config.yaml and MANIFEST.in.

## Conclusion

Adopt aiodocker if you are writing an asyncio service that has to create containers, stream their logs or watch Docker events, because the API maps onto the Engine API directly and the whole runtime dependency set is one package. Do not adopt it if you need a Docker management interface, because this is a client and not a daemon, a CLI or a panel, and it talks to a single engine rather than to a cluster. Verify first which Docker API version your engine exposes and whether the endpoints you need are covered, that the aiohttp version your environment has satisfies the stated floor and upper bound, and how you plan to expose the Docker socket to the process.

## FAQ

### How do I install aiodocker?

Run pip install aiodocker. The project requires Python 3.10 or newer, and its only runtime dependency is aiohttp>=3.11.13,<4.0. Optional extras include ssh for asyncssh and paramiko, plus dev, lint, test and doc groups for contributors.

### What are the aiodocker runtime dependencies and why is aiohttp pinned below 4.0?

The only runtime dependency is aiohttp>=3.11.13,<4.0. The lower bound exists because aiohttp 3.11.13 backported a fix for a break in a finally block that Python 3.14 turns into a SyntaxWarning, and the upper bound is interim because the vendored FlowControlDataQueue subclasses aiohttp.DataQueue, whose private surface may change in aiohttp 4.0.

### How do I create and run a container with aiodocker?

Use await docker.containers.create_or_replace with a config dict in the Docker API format, for example a Cmd list and an Image key, then await container.start(), read output with await container.log(stdout=True), and remove it with await container.delete(force=True).

### How do I set up a development environment for aiodocker?

The README recommends uv, which resolves dependencies from the committed uv.lock so every contributor and CI job builds against the same versions: uv sync --extra dev --extra lint --extra test --extra doc followed by uv run pre-commit install. A pip fallback is documented, with the note that the resulting environment is not pinned to uv.lock so versions may drift from CI.

### Can aiodocker talk to a remote Docker daemon?

Yes, through the ssh extra, which installs asyncssh and paramiko, with examples/ssh_example.py and examples/ssh_example_advanced.py showing the two forms. The library otherwise talks to the local Docker daemon, and the README documents examples/events.py for the Docker event stream and examples/stdio_stdout.py for attaching to container streams.

## Sources

- [aio-libs/aiodocker on GitHub](https://github.com/aio-libs/aiodocker)
- [Issues](https://github.com/aio-libs/aiodocker/issues)
- [License: Apache-2.0](https://github.com/aio-libs/aiodocker/blob/main/LICENSE)
- [README](https://github.com/aio-libs/aiodocker/blob/main/README.md)
- [Releases](https://github.com/aio-libs/aiodocker/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aio-libs-aiodocker
