Table of Contents #
Introduction #
There are plenty of frameworks for building AI agents, such as LangChain or LlamaIndex. But as you add models, tools, and features, the agent quickly grows into a lot of code to maintain. More code means more places for mistakes when you need to make a change.
docker-agent keeps the entire workflow in one YAML file. With this approach, you can mix and match models, tools, and features, and see how the whole agent is set up at a glance.
In this article, you will learn nice features of docker-agent by building an agent that answers questions about Polars.
π» Get the Code: The agent configs and eval files are on GitHub.
What Is docker-agent? #
docker-agent is Dockerβs open-source tool for building and running AI agents.
docker-agent does for AI agents what Docker did for apps. Instead of setting up an environment and writing code, you describe the agent in one YAML file and run it with one command.
Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.
Setup #
Install docker-agent with Homebrew, or use the docker agent command that ships with Docker Desktop 4.63 and later:
brew install docker-agent
The agent will search the Polars user guide and answer questions about it.
The project folder looks like this:
docker-agent-docs-qa/
βββ docs/polars/ # 72 markdown files the agent will search
Create the Agent #
To create the agent, define its model, instructions, and tools in a YAML file called embeddings.yaml. Start with the agents section.
agents:
root: # the agent that receives your message
model: openai/gpt-5-mini
description: Answers questions about Polars from the user guide
instruction: |
You answer questions about the Polars DataFrame library.
Always search the knowledge base before answering.
Answer only from what the search returns.
End with a "Sources:" list of the file paths you used, relative to docs/polars.
To let the agent search the docs, give it a rag tool and tell the tool which folder to search.
agents:
root:
...
toolsets:
- type: rag
ref: polars_docs # use the source defined below
rag:
polars_docs: # the name the ref points to
tool:
description: The Polars user guide
docs: [./docs/polars] # what to search
strategies: # how to search
- type: chunked-embeddings # split each file into chunks and embed them
embedding_model: openai/text-embedding-3-small
vector_dimensions: 1536 # number of values per vector
RAG setups usually need a vector database and a separate indexing step. docker-agent handles both for you. It stores the vectors in a local SQLite file, indexes the folder the first time the agent starts, and re-indexes files when they change.
rag is one of 30 built-in toolsets. Here are a few others that are useful:
shell: run shell commands, such as a Python script orduckdbfilesystem: read, write, and search files in a folderfetch: read web pages and online docsmcp: call external services like GitHub or adatabase through MCPmemory: remember facts across sessions
After the YAML file is created, ask the agent a question. The --exec flag runs the agent without opening the interactive terminal UI.
docker agent run --exec embeddings.yaml "How do I read a Parquet file without it all into memory?"
The output shows the search the agent made, followed by its answer. The search results and the modelβs reasoning summary are trimmed here:
Calling polars_docs(query: "read Parquet without all into memory scan_parquet
scan_parquet streaming lazy read_parquet memory efficient 'scan_parquet' 'read_parquet'
'scan' 'Parquet'")
Use a lazy scan and streaming collect so Polars parses and processes the file in
batches instead of materializing everything in RAM.
- Use pl.scan_parquet(...) (returns a LazyFrame) instead of pl.read_parquet(...).
Scanning delays parsing and enables column/row pruning via the optimizer
(projection & predicate pushdown).
- Apply selects/filters on the LazyFrame so the reader can skip unnecessary
columns/rows.
- When collecting, use streaming execution to process in batches:
.collect(engine="streaming").
Sources:
- io/parquet.md
- lazy/sources_sinks.md
- lazy/execution.md
The answer is correct, and every claim traces back to a file you can open.
To ask another question, run the same command with a new question without changing the YAML file.
docker agent run --exec embeddings.yaml "When should I use join_asof?"
Swap the Model #
To try a cheaper model without committing to it, you can simply pass --model for a single run.
embeddings.yaml --model what runs
βββββββββββββββββββββ βββββββββββββββββββββ
β model: gpt-5-mini β ββ gpt-5-nano ββ> β model: gpt-5-nano β
β instruction: ... β β instruction: ... β
β toolsets: rag β β toolsets: rag β
βββββββββββββββββββββ βββββββββββββββββββββ
unchanged this run only
For example, this runs the same question on gpt-5-nano.
docker agent run --exec embeddings.yaml --model openai/gpt-5-nano \
"How do I read a Parquet file without it all into memory?"
- Use scan_parquet to read lazily. This returns a LazyFrame and does not parse the
data into memory until you collect.
...
Sources:
io/parquet.md
This is very nice for several reasons:
- You can quickly try different models and compare their answers
- Because the YAML file does not change during the experiment, your teammates are not affected
- Once you like a model, you can edit the YAML file to use it permanently
Reuse Instructions Across Questions #
You might want to vary the instructions based on the context, such as the audience, the length, or the tone.
With docker-agent, you can write different instructions in separate files, then pick one with the --prompt-file flag.
For example, you can create a pandas-users.md file with the instructions for pandas users and a sql-users.md file with the instructions for SQL users.
"How do I read a Parquet file without it all into memory?"
β
βββββββββββββββββββββ΄ββββββββββββββββββββ
βΌ βΌ
--prompt-file pandas-users.md --prompt-file sql-users.md
β β
βΌ βΌ
Pandas: read_parquet() loads SQL: SELECT * FROM
the file into memory read_parquet('data.parquet')
Letβs try the pandas-users.md file first. Start by creating the file.
The reader knows pandas. Show the pandas equivalent next to each Polars answer.
Keep each answer under 8 lines.
Then start an interactive chat with the file passed to --prompt-file.
docker agent run embeddings.yaml --prompt-file pandas-users.md
Now every answer follows the instructions in pandas-users.md, even though the questions never mention pandas.
> How do I read a Parquet file without it all into memory?
Polars: use scan_parquet("file.parquet") to get a LazyFrame (parsing is delayed).
Collect with streaming to process in batches: lf.collect(engine="streaming").
...
Pandas: read_parquet("file.parquet") loads the file into memory (no built-in
lazy/streaming scan).
If your audience knows SQL instead, create a prompt file called sql-users.md.
The reader knows SQL. Show the Polars SQL equivalent next to each Polars answer.
Keep each answer under 8 lines.
Start a chat with that file instead.
docker agent run embeddings.yaml --prompt-file sql-users.md
The same question now gets a SQL answer instead of a pandas one.
> How do I read a Parquet file without it all into memory?
- Polars: Use lazy scanning so the file isn't parsed upfront:
lf = pl.scan_parquet("data.parquet"), which returns a LazyFrame.
SQL: SELECT * FROM read_parquet('data.parquet');
- Polars: Push down projections/filters so only needed columns/rows are read.
SQL: SELECT col1, col2 FROM read_parquet('data.parquet') WHERE ...;
...
Add Variants with Flavors #
Sometimes you want more than one version of an agent, such as one with a different search method or a different model. It is redundant to write another YAML file for each version with most of the settings staying the same.
With docker-agent, you can add a flavor to the same file. A flavor lists only the settings that change, and everything else stays the same.
--flavor hybrid
off on
β β
βΌ βΌ
ββββββββββββββββββββ ββββββββββββββββββββ
β embedding search β β embedding search β
β β β + keyword search β
ββββββββββββββββββββ ββββββββββββββββββββ
base config base + hybrid
In this example, we add a flavor called hybrid that adds keyword search to the existing embedding search strategy.
agents:
root: ...
rag:
polars_docs:
...
strategies:
- type: chunked-embeddings # embedding search
flavors: # new section at the bottom of the file
hybrid:
rag:
polars_docs:
strategies+: # keep chunked-embeddings and add bm25
- type: bm25 # keyword search
results:
fusion:
strategy: rrf # merge the keyword and embedding results
k: 60
deduplicate: true
limit: 5
To use a flavor, pass its name with --flavor:
docker agent run embeddings.yaml
docker agent run embeddings.yaml --flavor hybrid
Here is what the config looks like after docker-agent applies the hybrid flavor.
rag:
polars_docs:
...
strategies:
- type: chunked-embeddings # from the base file
- type: bm25 # from hybrid
results:
fusion:
strategy: rrf # from hybrid
...
To see how this is useful, ask about SQLContext, a class name that appears in the docs. Without the flavor, the agent uses the embedding search strategy and finds nothing.
docker agent run --exec embeddings.yaml "What is SQLContext?"
Calling polars_docs(query: "SQLContext")
Calling polars_docs(query: "SQL")
I searched the Polars documentation for "SQLContext" (and for "SQL") but found
no matching documents, so I cannot provide a definition from the docs.
With the hybrid flavor, every search runs both strategies. Keyword search finds the exact name, so the right page comes back.
docker agent run --exec embeddings.yaml --flavor hybrid "What is SQLContext?"
Calling polars_docs(query: "SQLContext")
SQLContext is the Polars object that manages SQL queries...
Sources:
- sql/intro.md
Run Different Agents from One File #
Sometimes you might want to have multiple agents for different purposes. For example, you might want to have a small model for answering questions and a stronger model for reviewing code.
docker-agent lets you add more agents to the same file. They can share tools already defined there, so you donβt need to repeat the configuration.
root reviewer
βββββββββββββββββββββ βββββββββββββββββββββ
β gpt-5-mini β β gpt-5 β
β answers questions β β reviews code β
βββββββββββ¬ββββββββββ βββββββββββ¬ββββββββββ
β β
ββββββββββββββββ βββββββββββββββββ
βΌ βΌ
βββββββββββββββββββ
β polars_docs β
β defined once β
βββββββββββββββββββ
In the code below, we add a second agent called reviewer that uses a stronger model for reviewing code but uses the same polars_docs tool as the root agent.
agents:
root:
model: openai/gpt-5-mini # small model for answering questions
instruction: |
You answer questions about the Polars DataFrame library.
...
toolsets:
- type: rag
ref: polars_docs # shared search
reviewer:
model: openai/gpt-5 # stronger model for the harder task
instruction: |
You review Polars code.
Search the docs for each function the code uses.
Point out slower or outdated patterns and suggest the fix the docs recommend.
...
toolsets:
- type: rag
ref: polars_docs # same shared search
By default, docker-agent runs the first agent in the file. To run the reviewer instead, pass --agent.
docker agent run --exec embeddings.yaml --agent reviewer \
'Review this code: df = pl.read_parquet("big.parquet").filter(pl.col("amount") > 0).select(["id", "amount"])'
Findings:
- Using read_parquet eagerly loads the entire file before your filter/select are
applied. This prevents predicate and projection pushdown, which is slower and
uses more memory on large Parquet files.
Recommended fix:
- Switch to scan_parquet and collect at the end:
df = (
pl.scan_parquet("big.parquet")
.filter(pl.col("amount") > 0)
.select(["id", "amount"])
.collect()
)
...
The reviewer spots the eager read_parquet call and suggests switching to scan_parquet.
Test the Agent #
As you experiment with different models and settings, you might want to make sure the agent keeps working as expected. It can be easy to miss mistakes by just observing the output.
docker-agents provides an easy way to define the test cases and run them against the agent.
evals/streaming-parquet.json
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β question: How do I read a Parquet file without β
β it all into memory? β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
βΌ
βββββββββββββββ
β agent β
ββββββββ¬βββββββ
βΌ
answer
β
ββββββββββββββββββββΌβββββββββββββββββββ
βΌ βΌ βΌ
β uses β mentions β searched
scan_parquet streaming the docs
To define a test case, create a JSON file in the evals/ folder, such as evals/streaming-parquet.json.
{
"title": "Large Parquet file question",
"messages": [
{"message": {"message": {"role": "user", "content": "How do I read a Parquet file without it all into memory?"}}}
],
"evals": {
"assertions": [
{ "name": "uses scan_parquet", "type": "contains", "value": "scan_parquet" },
{ "name": "mentions streaming", "type": "contains", "value": "streaming" },
{ "name": "searched the docs", "type": "tool_called", "value": "polars_docs" }
]
}
}
For this question to pass, the answer must mention scan_parquet and streaming, and the agent must have searched the docs.
Now run the suite with docker agent eval:
docker agent eval embeddings.yaml ./evals
β Asof join question ($0.001918)
β assertions 3/3
β Large Parquet file question ($0.002629)
β assertions 3/3
[ββββββββββ] 100% (2/2) β2 β0
β
Assertions: 6/6 passed (100.0%)
Total Cost: $0.004546
Total Time: 26s
Each run reports which tests passed, what each answer cost, and how long it took. Together, they tell you whether a change broke the agent, made it pricier, or slowed it down.
Since each eval runs in its own container, one test canβt leave files behind that change anotherβs result.
Compare variants before you switch
Because eval takes --flavor too, you can test a variant before adopting it. Letβs say you want to know whether the cheaper gpt-5-nano is good enough for your use case. Add it as a second flavor called cheap.
flavors:
hybrid: ...
cheap:
agents:
root:
model: openai/gpt-5-nano # smallest, lowest-cost GPT-5 model
Then compare the default model against the cheap flavor. Since answers vary between runs, --repeat 3 runs each test three times.
docker agent eval embeddings.yaml ./evals --repeat 3
docker agent eval embeddings.yaml ./evals --flavor cheap --repeat 3
gpt-5-mini gpt-5-nano (cheap)
assertions passed 18/18 13/18 <- less reliable
Parquet eval 3/3 runs 0/3 runs
asof join eval 3/3 runs 2/3 runs
total cost $0.015 $0.007 <- less than half the cost
The cheap model saves more than half the cost, but its answers are less reliable. It passed 13 of 18 checks, compared with 18 of 18 for the default model.
Share the Agent #
Once you are happy with a configuration, you might want to share it with your teammates.
Sharing a Python agent usually means your teammates have to download the code, install the dependencies, and run a setup script before they can use it.
docker-agent makes sharing agents as easy as sharing Docker images. Teammates run it with one command instead of setting up a Python project.
To push the YAML file to a registry such as Docker Hub (after docker login), run:
docker agent share push embeddings.yaml docker.io/<username>/polars-docs:v1
Teammates can then run the agent by name, without down the file first.
docker agent run docker.io/<username>/polars-docs:v1
To get a local copy of the YAML file instead, use docker agent share pull.
Note that only the YAML file is shared. Any files it points to, such as docs folders or prompt files, must exist in the folder where you run the agent.
Final Thoughts #
What I like about docker-agent is that it takes so little code to get a working agent. An LLM can help you write a Python agent, but you still have to maintain that code, and more code means more places for mistakes.
Keeping everything in one YAML file also makes agents easy to change and easy to read, even for people who do not write Python every day. And with Docker-style sharing, your teammates can try your agent with one command.
But docker-agent is not the right fit for everything. If you need to control each step the agent takes, with custom branches or retries, a code-first framework like LangGraph is a better fit.
The same principle of keeping configuration separate from code is a core chapter in Production-Ready Data Science.
References #
- docker-agent RAG docs (Docker, 2026): retrieval strategies, the default 0.5 similarity threshold, hybrid search with RRF fusion, and SQLite index storage.
Related Tutorials #
- Hydra for Python Configuration : The same βchange settings without editing codeβ idea for data pipelines
Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.