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 Image | Final Size | Security | Use Case |
|---|---|---|---|
golang:1.24 | ~1 GB | Low | Development only |
alpine:3.19 | ~20 MB | Medium | General production |
distroless/static | ~15 MB | High | Recommended for production |
scratch | ~12 MB | Highest | Minimal 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:
| Parameter | Purpose | Security Meaning |
|---|---|---|
--read-only | Root filesystem read-only | Prevents runtime modification of executables |
--tmpfs /tmp | In-memory tmpfs | Limits temporary file size; noexec prevents execution |
--memory | Memory limit | Prevents OOM from affecting the host |
--cpus | CPU limit | Prevents CPU starvation |
--pids-limit | Process limit | Prevents fork bombs |
--security-opt | No new privileges | Prevents container escape |
--cap-drop=ALL | Drop all capabilities | Principle 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:
- Use multi-stage builds: copy only compiled artifacts.
- Choose a minimal base image: distroless or alpine.
- Clean up caches: source code is not needed after
go mod download. - Reduce layer count: merge RUN instructions.
- 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:
- Incorrect
docker run -esyntax. - App uses an
.envfile that overrides environment variables. - 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.
