梦兽编程
AI_SUITE

Docker Containerized Deployment: Build Once, Run Anywhere

A detailed guide to packaging an ADK Go Agent into a Docker image, including multi-stage builds, image optimization, and production image configuration.

Docker containerization is the de-facto standard for modern application deployment. For Go applications, the advantage is even more obvious: statically compiled binaries can run in very small base images, with final images often under 20 MB. But containerization is more than docker build and docker run; production-grade container deployment requires image security, multi-stage build optimization, health checks, resource limits, and orchestration configuration.

This article systematically covers the complete containerization practice from Dockerfile authoring to production orchestration.


Dockerfile Authoring: From Good Enough to Optimal

Multi-Stage Build (Standard Practice)

Multi-stage builds, introduced in Docker 18.06, allow multiple FROM instructions in a single Dockerfile. Each stage can use a different base image, and only the required artifacts are copied into the final image.

# ============================================
# Stage 1: build environment
# ============================================
FROM golang:1.24-alpine AS builder

# install build dependencies
RUN apk add --no-cache git ca-certificates tzdata

# set working directory
WORKDIR /build

# copy go.mod and go.sum first to leverage Docker cache layers
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# copy source code
COPY . .

# build argument optimization
ARG VERSION=dev
ARG BUILD_TIME
ARG GIT_COMMIT

# production-grade build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-s -w \
        -X main.version=${VERSION} \
        -X main.buildTime=${BUILD_TIME} \
        -X main.gitCommit=${GIT_COMMIT} \
        -extldflags '-static'" \
    -trimpath \
    -o agent \
    .

# verify binary
RUN chmod +x agent && \
    ls -lh agent && \
    file agent

# ============================================
# Stage 2: runtime environment (distroless)
# ============================================
FROM gcr.io/distroless/static-debian12:nonroot

# copy timezone data from builder (Agent may need time-related logic)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# copy CA certificates (required for HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# copy compiled binary
COPY --from=builder /build/agent /agent

# run as non-root user (security hardening)
USER nonroot:nonroot

# expose port
EXPOSE 8080

# health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD ["/agent", "--health-check"] || exit 1

# start command
ENTRYPOINT ["/agent"]
CMD ["--port", "8080"]

Image size comparison:

Base ImageFinal SizeSecurityUse Case
golang:1.24~1 GBLowDevelopment only
alpine:3.19~20 MBMediumGeneral production
distroless/static~15 MBHighRecommended for production
scratch~12 MBHighestMinimal scenarios

Alpine Alternative

If you need a shell for debugging (distroless has no shell), use Alpine:

# ============================================
# Stage 2: Alpine runtime environment
# ============================================
FROM alpine:3.19

# install runtime dependencies
RUN apk add --no-cache ca-certificates tzdata curl

# create non-root user
RUN addgroup -g 1000 agent && \
    adduser -u 1000 -G agent -s /bin/sh -D agent

# copy binary
COPY --from=builder /build/agent /usr/local/bin/agent

# create data directory and set permissions
RUN mkdir -p /data && chown -R agent:agent /data

USER agent

WORKDIR /data

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

ENTRYPOINT ["agent"]
CMD ["--port", "8080"]

Build Script

#!/bin/bash
# build-docker.sh

set -e

VERSION=${VERSION:-$(git describe --tags --always)}
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
GIT_COMMIT=$(git rev-parse --short HEAD)
IMAGE_NAME="my-adk-agent"
REGISTRY="gcr.io/my-project"

echo "Building ${IMAGE_NAME}:${VERSION}..."

# build image
docker build \
    --build-arg VERSION="${VERSION}" \
    --build-arg BUILD_TIME="${BUILD_TIME}" \
    --build-arg GIT_COMMIT="${GIT_COMMIT}" \
    -t "${IMAGE_NAME}:${VERSION}" \
    -t "${IMAGE_NAME}:latest" \
    .

# security scan (using Trivy)
if command -v trivy &> /dev/null; then
    echo "Scanning image for vulnerabilities..."
    trivy image --severity HIGH,CRITICAL "${IMAGE_NAME}:${VERSION}"
fi

# push image (optional)
if [ "${PUSH:-false}" = "true" ]; then
    docker tag "${IMAGE_NAME}:${VERSION}" "${REGISTRY}/${IMAGE_NAME}:${VERSION}"
    docker tag "${IMAGE_NAME}:latest" "${REGISTRY}/${IMAGE_NAME}:latest"
    docker push "${REGISTRY}/${IMAGE_NAME}:${VERSION}"
    docker push "${REGISTRY}/${IMAGE_NAME}:latest"
fi

echo "Build complete: ${IMAGE_NAME}:${VERSION}"

Build and Run: From Local to Production

Local Run

# build
docker build -t my-adk-agent:latest .

# run (development mode)
docker run -d \
    --name my-agent \
    -p 8080:8080 \
    -e GOOGLE_API_KEY="${GOOGLE_API_KEY}" \
    -e LOG_LEVEL=debug \
    -v $(pwd)/config.yaml:/data/config.yaml:ro \
    my-adk-agent:latest \
    --config /data/config.yaml

# view logs
docker logs -f my-agent

# enter container for debugging (Alpine images only)
docker exec -it my-agent /bin/sh

# stop and remove
docker stop my-agent && docker rm my-agent

Production Run

# production run (more restrictions)
docker run -d \
    --name my-agent \
    --restart unless-stopped \
    --read-only \
    --tmpfs /tmp:noexec,nosuid,size=100m \
    -p 8080:8080 \
    -e GOOGLE_API_KEY="${GOOGLE_API_KEY}" \
    -e LOG_LEVEL=info \
    -e LOG_FORMAT=json \
    -e MAX_CONCURRENT=200 \
    -e MAX_SESSIONS=50000 \
    --memory=2g \
    --memory-swap=2g \
    --cpus=2.0 \
    --pids-limit=10000 \
    --security-opt=no-new-privileges:true \
    --cap-drop=ALL \
    --cap-add=NET_BIND_SERVICE \
    my-adk-agent:latest

Security parameter reference:

ParameterPurposeSecurity Meaning
--read-onlyRoot filesystem read-onlyPrevents runtime modification of executables
--tmpfs /tmpIn-memory tmpfsLimits temporary file size; noexec prevents execution
--memoryMemory limitPrevents OOM from affecting the host
--cpusCPU limitPrevents CPU starvation
--pids-limitProcess limitPrevents fork bombs
--security-optNo new privilegesPrevents container escape
--cap-drop=ALLDrop all capabilitiesPrinciple of least privilege

Docker Compose: Bridge from Development to Production

Basic Configuration

# docker-compose.yml
version: '3.8'

services:
  agent:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        VERSION: ${VERSION:-latest}
    image: my-adk-agent:${VERSION:-latest}
    container_name: my-agent
    restart: unless-stopped

    ports:
      - "8080:8080"

    environment:
      - PORT=8080
      - LOG_LEVEL=info
      - LOG_FORMAT=json
      - MAX_CONCURRENT=200
      - MAX_SESSIONS=50000
      - SESSION_TTL=24h
      - REDIS_URL=redis://redis:6379/0
      - GOOGLE_API_KEY=${GOOGLE_API_KEY}

    volumes:
      - ./config.yaml:/data/config.yaml:ro
      - agent-data:/data/sessions

    networks:
      - agent-network

    depends_on:
      redis:
        condition: service_healthy

    healthcheck:
      test: ["CMD", "/agent", "--health-check"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

    # resource limits
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3

    # security options
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

  redis:
    image: redis:7-alpine
    container_name: agent-redis
    restart: unless-stopped

    volumes:
      - redis-data:/data

    networks:
      - agent-network

    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  nginx:
    image: nginx:alpine
    container_name: agent-nginx
    restart: unless-stopped

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
      - ./webui/dist:/usr/share/nginx/html:ro

    networks:
      - agent-network

    depends_on:
      - agent

volumes:
  agent-data:
  redis-data:

networks:
  agent-network:
    driver: bridge

Production Override Configuration

# docker-compose.prod.yml
version: '3.8'

services:
  agent:
    # production does not rebuild; use a prebuilt image
    build: !reset null
    image: gcr.io/my-project/my-adk-agent:v1.2.3

    environment:
      - LOG_LEVEL=warn
      - LOG_FORMAT=json
      - MAX_CONCURRENT=500
      - METRICS_PORT=9090

    # stricter resource limits in production
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 1G
      replicas: 3  # Swarm mode

    # production uses external load balancer; do not expose ports directly
    ports: !reset []

# start command:
# docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Image Security: From Build to Runtime

Image Scanning

# scan vulnerabilities with Trivy
trivy image my-adk-agent:latest

# show only HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL my-adk-agent:latest

# generate report
trivy image --format json -o report.json my-adk-agent:latest

# use Snyk
snyk container test my-adk-agent:latest

Image Signing (Cosign)

# install cosign
# https://docs.sigstore.dev/cosign/installation

# generate key pair
cosign generate-key-pair

# sign image
cosign sign --key cosign.key gcr.io/my-project/my-adk-agent:v1.2.3

# verify signature
cosign verify --key cosign.pub gcr.io/my-project/my-adk-agent:v1.2.3

Minimize Attack Surface

# 1. use non-root user
USER nonroot:nonroot

# 2. copy only required files
COPY --from=builder /build/agent /agent

# 3. no package manager (distroless satisfies this naturally)
# 4. expose only necessary ports
EXPOSE 8080

# 5. no shell (distroless has no shell)
# 6. read-only filesystem (runtime configuration)

In-Depth Troubleshooting

Q: Image Is Too Large

Diagnosis:

# view image layers
docker history my-adk-agent:latest

# analyze image contents
docker run --rm -it my-adk-agent:latest ls -la /

# analyze with dive
dive my-adk-agent:latest

Optimization strategies:

  1. Use multi-stage builds: copy only compiled artifacts.
  2. Choose a minimal base image: distroless or alpine.
  3. Clean up caches: source code is not needed after go mod download.
  4. Reduce layer count: merge RUN instructions.
  5. Exclude irrelevant files: use .dockerignore.
# .dockerignore
.git
.gitignore
*.md
docker-compose*.yml
Dockerfile*
.env
.env.example
vendor/
dist/
*.test
*.out

Q: Timezone Not Found in Container

Root cause: Minimal images (e.g., distroless, scratch) do not include timezone data.

Solution:

# copy timezone data from builder
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# set default timezone (optional)
ENV TZ=Asia/Shanghai

Q: Environment Variables Not Passed In

Diagnosis:

# view container environment variables
docker exec my-agent env

# view actual config read by the app
docker exec my-agent /agent --dump-config

Common causes:

  1. Incorrect docker run -e syntax.
  2. App uses an .env file that overrides environment variables.
  3. Environment variable name case mismatch.

Best practices:

# docker-compose.yml
services:
  agent:
    env_file:
      - .env  # base config
    environment:
      - GOOGLE_API_KEY=${GOOGLE_API_KEY}  # passed from host env
# .env file (do not commit to Git)
LOG_LEVEL=info
MAX_CONCURRENT=200

# pass secrets via host environment variables
# export GOOGLE_API_KEY=xxx

Next Steps

Docker deployment is done; next, let’s look at cloud deployment — Cloud Run and GKE.

Web Deployment | Cloud Run / GKE Deployment →


Want to learn more Go ADK hands-on tips? Follow the “Full Stack Summit - Mengshou Programming” WeChat official account for weekly Go / AI programming updates.

Frequently Asked Questions

Why are Go applications especially well-suited for Docker containerization?

Go's statically compiled binary can run in extremely small base images such as scratch, distroless, or Alpine.

What is the core idea of multi-stage builds?

Use multiple FROM stages in one Dockerfile, copying only the compiled artifact into the final image to minimize size.

What base images are recommended for production?

distroless/static is recommended; Alpine is an alternative if shell debugging is needed; scratch is for minimal scenarios.

What security measures should be applied to a container?

Use non-root user, read-only root filesystem, minimal capabilities, resource limits, and regular vulnerability scanning.

How should environment variables and config be managed in containers?

Use env vars for sensitive/runtime config and config files for static config; combine with Docker Compose for orchestration.