# Containerizing an ADK agent


So far we’ve run the trading agent two ways: locally with `adk run`, and [deployed to Agent Platform runtime](/ap/1-agent/1.2-deploy-to-ap-runtime/) with a little Python script that packages the code and hosts it for us. Both are handy, but in each case something else is deciding how your agent gets hosted. Sometimes you just want a plain container — one image you can run on your laptop today, and on Cloud Run, GKE, or Agent Platform runtime (in bring-your-own-container mode) tomorrow. Same artifact, runs anywhere Docker does.

This post kicks off the containerizing section: we’ll wrap the agent up as a container and run it locally. (As always with this agent, these are paper trades — but they do execute, so don’t point it at a funded account.)

The one shift in mindset is how we serve it. Instead of `adk run`, which gives you an interactive CLI, we’ll use `adk api_server` to expose the agent as an HTTP API on port 8080. That’s the thing we put in a container.

## The Dockerfile

Nothing exotic here — a slim Python base, install the requirements, copy in the agent, and set the start command:

```dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY trading_agent/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN mkdir -p trading_agent

COPY trading_agent/agent.py trading_agent/

CMD ["sh", "-c", "adk api_server --host 0.0.0.0 --port ${PORT:-8080} ${SESSION_SERVICE_URI:+--session_service_uri $SESSION_SERVICE_URI} ${MEMORY_SERVICE_URI:+--memory_service_uri $MEMORY_SERVICE_URI} --no-reload /app"]
```

*[Dockerfile](https://github.com/WilliamDenniss/agent-examples/blob/master/05_Containerized/Dockerfile)*

The `CMD` is the only part worth slowing down on. `${PORT:-8080}` defaults the port to 8080, but honors `$PORT` if the platform sets one (Cloud Run injects it, for example). The two `${SESSION_SERVICE_URI:+…}` expansions are the neat bit: that syntax only adds the flag *when the variable is set*. So the very same image runs two ways — with nothing set it’s a self-contained agent with in-memory sessions, and set those two URIs and it wires straight into a managed Sessions and Memory backend instead. One image, both modes.

## Building and running locally

Drop your keys into a `docker-env` file. These get passed in at run time with `--env-file`, so they never get baked into the image:

```shell
# Alpaca API keys
APCA_API_KEY_ID=your-key-id
APCA_API_SECRET_KEY=your-key

# Google AI Studio key for the ADK agent
GOOGLE_API_KEY=your-key
```

Then build and run — that’s the two lines in `docker.sh`:

```shell
docker build . -t trading_agent
docker run -it --rm --env-file docker-env -p 8080:8080 trading_agent
```

*[docker.sh](https://github.com/WilliamDenniss/agent-examples/blob/master/05_Containerized/docker.sh)*

Since we didn’t set `SESSION_SERVICE_URI` or `MEMORY_SERVICE_URI`, this is the self-contained mode: the agent serves on `http://localhost:8080` with in-memory sessions, so its memory resets when the container stops. That’s fine for checking everything works.

## Trying it out

`adk api_server` is an API, not a chat prompt, so to exercise it we create a session and post a message to the `/run` endpoint. There’s a small `trade-docker.sh` that does exactly that against `localhost:8080`, then pulls the agent’s final reply out of the response:

<details class="expander">
  <summary class="expander-summary">trade-docker.sh</summary>
  <div class="expander-content">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Run the agent locally in Docker first:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   docker build -t trade .</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   docker run -it --rm --env-file docker-env -p 8080:8080 trade</span>
</span></span><span style="display:flex;"><span>SERVICE_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://localhost:8080&#34;</span>
</span></span><span style="display:flex;"><span>APP_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;trading_agent&#34;</span>
</span></span><span style="display:flex;"><span>USER_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;user1&#34;</span>
</span></span><span style="display:flex;"><span>MESSAGE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1<span style="color:#66d9ef">:-</span>Run the trading cycle<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Creating session...&#34;</span>
</span></span><span style="display:flex;"><span>SESSION_ID<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s -X POST <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{&#34;state&#34;: {}}&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;</span>$SERVICE_URL<span style="color:#e6db74">/apps/</span>$APP_NAME<span style="color:#e6db74">/users/</span>$USER_ID<span style="color:#e6db74">/sessions&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  | python3 -c <span style="color:#e6db74">&#34;import sys,json; print(json.load(sys.stdin)[&#39;id&#39;])&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Session: </span>$SESSION_ID<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Running trading cycle...&#34;</span>
</span></span><span style="display:flex;"><span>curl -s -X POST <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#34;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    \&#34;appName\&#34;: \&#34;</span>$APP_NAME<span style="color:#e6db74">\&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    \&#34;userId\&#34;: \&#34;</span>$USER_ID<span style="color:#e6db74">\&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    \&#34;sessionId\&#34;: \&#34;</span>$SESSION_ID<span style="color:#e6db74">\&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    \&#34;newMessage\&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      \&#34;role\&#34;: \&#34;user\&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      \&#34;parts\&#34;: [{\&#34;text\&#34;: \&#34;</span>$MESSAGE<span style="color:#e6db74">\&#34;}]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;</span>$SERVICE_URL<span style="color:#e6db74">/run&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  | python3 -c <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import sys, json
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">events = json.loads(sys.stdin.read())
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">for event in reversed(events):
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    content = event.get(&#39;content&#39;, {})
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    parts = content.get(&#39;parts&#39;, [])
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    for part in parts:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if &#39;text&#39; in part and content.get(&#39;role&#39;) == &#39;model&#39;:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            print(part[&#39;text&#39;])
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            exit()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p><em><a href="https://github.com/WilliamDenniss/agent-examples/blob/master/05_Containerized/trade-docker.sh">trade-docker.sh</a></em></p>
  </div>
</details>


With the container running in one terminal, run it from another:

```shell
./trade-docker.sh
```

It creates a session, kicks off the cycle, and prints the agent’s final reply — the same end-of-cycle report we saw back when we ran it with [`adk run`](/ap/1-agent/1.1-create-an-adk-agent/), just served over HTTP this time.

Tip: the script takes an optional message, so `./trade-docker.sh "go"` works too if you’d rather not type the default.

## Connecting to a memory backend

The local container forgets everything when it stops. To give it persistent Sessions and Memory instead, we flip on the two URIs from the `CMD` — and that’s what `docker-env-remote` and `docker-remote.sh` are for.

The remote env file points at Vertex AI and a managed backend resource:

```shell
# Alpaca API keys
APCA_API_KEY_ID=your-key-id
APCA_API_SECRET_KEY=your-key

GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=your-project
GOOGLE_CLOUD_LOCATION=us-west1

SESSION_SERVICE_URI=agentengine://projects/your-project/locations/us-west1/reasoningEngines/RESOURCE_ID
MEMORY_SERVICE_URI=agentengine://projects/your-project/locations/us-west1/reasoningEngines/RESOURCE_ID
```

Warning: on Vertex AI you authenticate with your Google Cloud identity, not an API key. Set `GOOGLE_GENAI_USE_VERTEXAI=TRUE` and make sure there’s **no** `GOOGLE_API_KEY` or `GEMINI_API_KEY` in this file. It’s the same point as the [managed deploy](/ap/1-agent/1.2-deploy-to-ap-runtime/): the API key is a long-lived dev token, and in this mode we use your real identity instead.

To get that identity into the container, `docker-remote.sh` mounts your local gcloud config read-only:

```shell
docker build . -t trading_agent

docker run -it --rm \
  --env-file docker-env-remote \
  -v ~/.config/gcloud:/root/.config/gcloud:ro \
  -p 8080:8080 trading_agent
```

*[docker-remote.sh](https://github.com/WilliamDenniss/agent-examples/blob/master/05_Containerized/docker-remote.sh)*

That `-v ~/.config/gcloud:…:ro` is the bit that lets the containerized agent call Google Cloud as you, without baking any credentials into the image — handy for local testing. (In production you’d give the container its own service account instead.)

The one missing piece is the resource those `agentengine://` URIs point at. That’s an Agent Platform resource (currently called an “agent engine”), and creating one is exactly what we do next.

## What’s next

We’ve now got a single container image that runs fully self-contained on your laptop, and — with a couple of env vars — talks to a managed memory backend instead. That portability is the whole point: this same image is what we’ll push to Cloud Run, GKE, and Agent Platform runtime later in this section.

First, though, we need that backend resource. Next up: [creating an Agent Platform resource](/ap/3-containerizing/3.2-creating-an-ap-resource/).

