Go Engineer Comprehensive Course 015

Docker Containerization — A Practical Guide for Go Projects


I. Docker Core Concepts

1.1 What is Docker

Docker is an open-source containerization platform that packages applications and all their dependencies into a standardized unit (container), achieving "build once, run anywhere". For Go developers, Docker addresses the following pain points:

  • Inconsistent development and production environments
  • Complex dependency management (databases, caches, message queues, and other middleware)
  • Non-standardized deployment process
  • Difficulty in multi-service collaboration in a microservices architecture

1.2 Three Core Concepts

Image

An image is a read-only template that contains all the file system layers required to run an application. It can be compared to a "Class":

  • An image is composed of multiple layers, each representing an instruction in the Dockerfile
  • Layers are read-only and reusable; multiple images can share underlying layers
  • Images are versioned using tag, e.g., golang:1.22-alpine
+---------------------------+
|   应用代码层 (COPY .)      |  <-- 最上层,变化最频繁
+---------------------------+
|   依赖安装层 (go mod)      |
+---------------------------+
|   基础工具层 (RUN apk)     |
+---------------------------+
|   基础镜像层 (alpine)      |  <-- 最底层,最稳定
+---------------------------+

Container

A container is a running instance of an image, comparable to an "Object":

  • A container adds a writable layer on top of an image
  • Each container has its own independent file system, network, and process space
  • Containers are temporary and stateless; data in the writable layer is lost after destruction
  • Data requiring persistence should use volumes

Registry

A registry is a service for storing and distributing images:

  • Docker Hub: The official public registry, similar to GitHub
  • Private Registries: Such as Harbor, AWS ECR, Alibaba Cloud ACR
  • Images are identified by the format registry/repository:tag, e.g., docker.io/library/golang:1.22

II. Dockerfile Writing (Go Project Example)

2.1 Basic Dockerfile

Suppose we have a simple Go Web service:

// main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "OK")
    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go Docker Service!")
    })

    log.Printf("Server starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

The simplest but not recommended Dockerfile:

# 不推荐:镜像体积大,包含编译工具链
FROM golang:1.22

WORKDIR /app
COPY . .
RUN go build -o server .

EXPOSE 8080
CMD ["./server"]

Images generated this way typically exceed 800MB because they include the complete Go toolchain.

2.2 Multi-stage Build

Multi-stage build is a core technique for Dockerizing Go projects, which can reduce image size from 800MB+ to 10-20MB:

# ============ 第一阶段:编译 ============
FROM golang:1.22-alpine AS builder

# 设置 Go 模块代理(国内加速)
ENV GOPROXY=https://goproxy.cn,direct

WORKDIR /app

# 先复制依赖文件,利用缓存层(详见 2.3 节)
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码并编译
COPY . .

# CGO_ENABLED=0 生成静态链接的二进制文件
# -ldflags="-s -w" 去除调试信息,减小体积
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w" \
    -o /app/server .

# ============ 第二阶段:运行 ============
FROM alpine:3.19

# 安装必要的 CA 证书(HTTPS 请求需要)和时区数据
RUN apk --no-cache add ca-certificates tzdata

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 从 builder 阶段复制编译好的二进制文件
COPY --from=builder /app/server .

# 复制配置文件(如果有)
# COPY --from=builder /app/config ./config

# 切换到非 root 用户
USER appuser

EXPOSE 8080

# 使用 ENTRYPOINT 而非 CMD,防止被意外覆盖
ENTRYPOINT ["./server"]

More extreme solution -- using a scratch empty image:

# 编译阶段同上...

# 使用空镜像,最终镜像仅包含二进制文件
FROM scratch

# 从 builder 复制 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 从 builder 复制时区信息
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

Image size comparison:

Base Image Final Size Applicable Scenarios
golang:1.22 ~850MB Not recommended for production
alpine:3.19 ~15MB Recommended, supports shell debugging
scratch ~8MB Extremely minimal, no shell
distroless ~12MB Google recommended, good security

2.3 Build Cache Optimization

When Docker builds, each layer is cached. As long as the input for a layer hasn't changed, the cache will be reused. The key principle is: place layers with low frequency of change first, and layers with high frequency of change later.

# 好的做法:分离依赖下载和代码编译
COPY go.mod go.sum ./       # 依赖文件变化少,缓存命中率高
RUN go mod download          # 只有依赖变化时才重新下载
COPY . .                     # 代码频繁变化
RUN go build -o server .     # 每次代码变化都重新编译

# 坏的做法:一次性复制所有文件
COPY . .                     # 任何文件变化都导致后续所有层缓存失效
RUN go mod download
RUN go build -o server .

2.4 Best Practices Summary

.dockerignore File

Create a .dockerignore file in the project root directory to reduce build context size:

# .dockerignore
.git
.gitignore
.idea
.vscode
*.md
README*
LICENSE
Makefile
docker-compose*.yml
Dockerfile*
tmp/
vendor/
bin/
*.test
*.prof

Security Best Practices

# 1. Use specific version tags, avoid 'latest'
FROM alpine:3.19    # 好
FROM alpine:latest  # 坏 -- 不可复现

# 2. Run as a non-root user
RUN addgroup -S app && adduser -S app -G app
USER app

# 3. Read-only file system (specified at runtime)
# docker run --read-only --tmpfs /tmp myapp

# 4. Do not store secrets in the image
# Bad practice
ENV DB_PASSWORD=secret123
# Good practice: Inject at runtime via environment variables or Secret management tools

III. docker-compose Orchestration

3.1 Why docker-compose is Needed

In a microservices architecture, a Go project often depends on multiple external services. docker-compose allows defining and managing the orchestration of multiple containers using a single YAML file.

3.2 Complete Example: Go Service + MySQL + Redis + Consul

# docker-compose.yml
version: "3.9"

services:
  # ============ Go Application Service ============
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - GOPROXY=https://goproxy.cn,direct
    container_name: go-app
    ports:
      - "8080:8080"    # Host Port:Container Port
    environment:
      - PORT=8080
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=rootpassword
      - DB_NAME=myapp
      - REDIS_ADDR=redis:6379
      - CONSUL_ADDR=consul:8500
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      consul:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped
    # 资源限制
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

  # ============ MySQL Database ============
  mysql:
    image: mysql:8.0
    container_name: go-mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: myapp
      MYSQL_CHARSET: utf8mb4
    volumes:
      - mysql-data:/var/lib/mysql              # Data persistence
      - ./deploy/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql  # Initialization script
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ============ Redis Cache ============
  redis:
    image: redis:7-alpine
    container_name: go-redis
    ports:
      - "6379:6379"
    command: redis-server --requirepass redispassword --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redispassword", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ============ Consul Service Registration and Discovery ============
  consul:
    image: hashicorp/consul:1.17
    container_name: go-consul
    ports:
      - "8500:8500"    # HTTP API and Web UI
      - "8600:8600/udp" # DNS
    command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
    volumes:
      - consul-data:/consul/data
    networks:
      - app-network
    restart: unless-stopped

# ============ Volume Definitions ============
volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
  consul-data:
    driver: local

# ============ Network Definitions ============
networks:
  app-network:
    driver: bridge

3.3 Common docker-compose Commands

# Start all services (run in background)
docker-compose up -d

# Start and rebuild images
docker-compose up -d --build

# View service status
docker-compose ps

# View logs for a specific service
docker-compose logs -f app

# Stop all services
docker-compose down

# Stop and remove volumes (use with caution, data will be lost)
docker-compose down -v

# Start only a specific service and its dependencies
docker-compose up -d app

# Enter a container for a specific service
docker-compose exec app sh

# Scale service instances
docker-compose up -d --scale app=3

IV. Quick Reference for Common Docker Commands

4.1 Image Management

# Build image
docker build -t myapp:v1.0 .
docker build -t myapp:v1.0 -f deploy/Dockerfile .   # Specify Dockerfile path

# View local images
docker images
docker image ls

# Delete image
docker rmi myapp:v1.0
docker image prune           # Clean up dangling images
docker image prune -a        # Clean up all unused images

# Image tag
docker tag myapp:v1.0 registry.example.com/myapp:v1.0

# Import/Export (offline scenarios)
docker save -o myapp.tar myapp:v1.0
docker load -i myapp.tar

# View image build history / layer information
docker history myapp:v1.0
docker inspect myapp:v1.0

4.2 Container Management

# Run container
docker run -d --name myapp -p 8080:8080 myapp:v1.0
docker run -d --name myapp \
  -p 8080:8080 \
  -e DB_HOST=localhost \
  -v /host/data:/app/data \
  --restart unless-stopped \
  myapp:v1.0

# View running containers
docker ps
docker ps -a                 # Include stopped containers

# Container lifecycle
docker start myapp
docker stop myapp
docker restart myapp
docker rm myapp              # Delete stopped container
docker rm -f myapp           # Force delete (including running ones)

# Enter container internal
docker exec -it myapp sh     # For alpine use sh
docker exec -it myapp bash   # For images with bash use bash

# View logs
docker logs myapp
docker logs -f myapp         # Real-time follow
docker logs --tail 100 myapp # Last 100 lines

# View container resource usage
docker stats
docker stats myapp

# Copy files
docker cp myapp:/app/logs/app.log ./app.log   # Container to host
docker cp ./config.yaml myapp:/app/config.yaml # Host to container

4.3 System Cleanup

# One-click cleanup: stopped containers, dangling images, unused networks
docker system prune

# Include unused volumes (use with caution)
docker system prune --volumes

# View Docker disk usage
docker system df

V. Docker Network Modes

5.1 Bridge Mode (Default)

Each container has its own independent network namespace, communicating via the virtual bridge docker0:

# Create custom bridge network
docker network create my-network

# Connect containers to custom network
docker run -d --name app --network my-network myapp:v1.0
docker run -d --name db --network my-network mysql:8.0

# Containers within the same network can access each other by name
# Inside the app container, "db:3306" can be used to connect to the database

Advantages of custom bridge networks:
- Containers discover each other by name (DNS)
- Better network isolation
- Ability to dynamically connect/disconnect containers

5.2 Host Mode

Containers directly use the host's network, with no network isolation:

docker run -d --network host myapp:v1.0
# Container listening on port 8080 = Host port 8080
# No -p port mapping needed
  • Pros: Best performance, no NAT overhead
  • Cons: Port conflict risk, lower security
  • Use cases: High-performance network requirements, network debugging

5.3 None Mode

Containers have no network connectivity, completely isolated:

docker run -d --network none myapp:v1.0
  • Use cases: Tasks that only require computation and no network

5.4 Network Mode Comparison

Mode Isolation Performance Use Cases
bridge High Medium Default choice, most scenarios
host None High High-performance requirements
none Complete - Security-sensitive computational tasks
overlay High Medium Cross-host Swarm/K8s

VI. Volumes and Persistence

6.1 Why Volumes are Needed

A container's file system is temporary, and data is lost when the container is deleted. Volumes provide a persistent storage mechanism.

6.2 Three Mount Types

# 1. Named Volume -- Recommended for data persistence
docker volume create mydata
docker run -v mydata:/app/data myapp:v1.0

# 2. Bind Mount -- Suitable for mounting source code during development
docker run -v /host/path:/container/path myapp:v1.0
docker run -v $(pwd)/config:/app/config:ro myapp:v1.0   # Read-only mount

# 3. tmpfs Mount -- Temporary file system in memory
docker run --tmpfs /app/tmp myapp:v1.0

6.3 Volume Management

# View all volumes
docker volume ls

# View volume details
docker volume inspect mydata

# Delete volume
docker volume rm mydata

# Clean up unused volumes
docker volume prune

6.4 Typical Usage in Go Projects

# Volume usage in docker-compose.yml
services:
  app:
    volumes:
      - ./config:/app/config:ro    # Configuration files (read-only bind)
      - app-logs:/app/logs         # Log directory (named volume)
      - /app/tmp                   # Anonymous volume (temporary files)

  mysql:
    volumes:
      - mysql-data:/var/lib/mysql  # Database data (named volume, most important)

volumes:
  app-logs:
  mysql-data:

VII. Docker's Application in Microservices Development

7.1 Local Development Workflow

In a microservices architecture, a Go service may depend on multiple other services. Docker makes local development easy:

+-----------------------------------------------+
|           docker-compose orchestration        |
|                                               |
|  +----------+  +----------+  +----------+    |
|  | User Service |  | Order Service |  | Product Service |    |
|  | Go :8081 |  | Go :8082 |  | Go :8083 |    |
|  +----+-----+  +----+-----+  +----+-----+    |
|       |              |              |          |
|  +----+--------------+--------------+----+    |
|  |          app-network (bridge)         |    |
|  +----+----------+----------+-----------+    |
|       |          |          |                 |
|  +----+---+ +---+----+ +--+------+           |
|  | MySQL  | | Redis  | | Consul  |           |
|  | :3306  | | :6379  | | :8500   |           |
|  +--------+ +--------+ +---------+           |
+-----------------------------------------------+

7.2 Hot Reload Development Mode

During development, tools like air can be combined to achieve code hot reloading:

# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app                  # Mount source code
    command: air                 # Use air for hot reload
    ports:
      - "8080:8080"
# Dockerfile.dev
FROM golang:1.22-alpine

RUN go install github.com/air-verse/air@latest

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

# No need for COPY . . because source code is mounted via volume
CMD ["air"]

7.3 Integration Testing

Docker makes it easy to set up an integration testing environment:

# Start test dependencies
docker-compose -f docker-compose.test.yml up -d

# Run integration tests
go test ./... -tags=integration

# Cleanup
docker-compose -f docker-compose.test.yml down -v

You can also dynamically create test containers in Go code using the testcontainers-go library:

package repository_test

import (
    "context"
    "testing"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/mysql"
)

func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    // Dynamically create MySQL container for testing
    mysqlC, err := mysql.Run(ctx,
        "mysql:8.0",
        mysql.WithDatabase("testdb"),
        mysql.WithUsername("test"),
        mysql.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer mysqlC.Terminate(ctx)

    // Get connection address
    host, _ := mysqlC.Host(ctx)
    port, _ := mysqlC.MappedPort(ctx, "3306")

    // Use a real database for testing...
    t.Logf("MySQL running at %s:%s", host, port.Port())
}

VIII. Pushing Images to Registry

8.1 Pushing to Docker Hub

# 1. Login
docker login

# 2. Tag the image (format: username/repository:tag)
docker tag myapp:v1.0 username/myapp:v1.0
docker tag myapp:v1.0 username/myapp:latest

# 3. Push
docker push username/myapp:v1.0
docker push username/myapp:latest

8.2 Pushing to a Private Registry

# Taking Alibaba Cloud ACR as an example
# 1. Login to private registry
docker login registry.cn-hangzhou.aliyuncs.com

# 2. Tag
docker tag myapp:v1.0 registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0

# 3. Push
docker push registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0

8.3 Setting up a Local Private Registry (Harbor)

Harbor is an enterprise-grade Docker image registry that supports features like access control and image scanning:

# Quickly deploy Harbor using Docker Compose
# After downloading the offline installation package:
./install.sh --with-chartmuseum --with-trivy

# Push to Harbor
docker tag myapp:v1.0 harbor.example.com/myproject/myapp:v1.0
docker push harbor.example.com/myproject/myapp:v1.0

8.4 Automated Build and Push in CI/CD

Automatically build and push images in GitHub Actions:

# .github/workflows/docker.yml
name: Build and Push Docker Image

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            username/myapp:${{ github.ref_name }}
            username/myapp:latest

IX. Complete Practical Workflow for Dockerizing Go Projects

9.1 Project Structure

myapp/
├── cmd/
│   └── server/
│       └── main.go          # Entrypoint
├── internal/
│   ├── handler/             # HTTP Handlers
│   ├── service/             # Business Logic
│   ├── repository/          # Data Access
│   └── config/              # Configuration Management
├── deploy/
│   ├── mysql/
│   │   └── init.sql         # Database Initialization
│   └── nginx/
│       └── nginx.conf       # Reverse Proxy Configuration
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── .dockerignore
└── Makefile

9.2 Makefile Automation

# Makefile
APP_NAME=myapp
VERSION=$(shell git describe --tags --always --dirty)
REGISTRY=registry.example.com/myns

.PHONY: build run test docker-build docker-push deploy clean

# Local build
build:
    CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" \
        -o bin/$(APP_NAME) ./cmd/server

# Local run
run:
    go run ./cmd/server

# Run tests
test:
    go test ./... -v -cover

# Build Docker image
docker-build:
    docker build -t $(APP_NAME):$(VERSION) .
    docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest

# Push image
docker-push: docker-build
    docker tag $(APP_NAME):$(VERSION) $(REGISTRY)/$(APP_NAME):$(VERSION)
    docker push $(REGISTRY)/$(APP_NAME):$(VERSION)

# Start development environment
dev:
    docker-compose -f docker-compose.dev.yml up -d --build

# Start production environment
deploy:
    docker-compose up -d --build

# View logs
logs:
    docker-compose logs -f app

# Cleanup
clean:
    docker-compose down -v
    docker image prune -f
    rm -rf bin/

9.3 Complete Production-Grade Dockerfile

# ============ Stage 1: Build ============
FROM golang:1.22-alpine AS builder

# Install necessary build tools
RUN apk add --no-cache git ca-certificates tzdata

# Set up Go environment
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

WORKDIR /build

# Dependency cache layer
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Compile
COPY . .
ARG VERSION=dev
RUN go build \
    -ldflags="-s -w -X main.Version=${VERSION}" \
    -o /build/app \
    ./cmd/server

# ============ Stage 2: Run ============
FROM alpine:3.19

LABEL maintainer="your-email@example.com"
LABEL version="${VERSION}"
LABEL description="My Go Application"

# Install runtime dependencies
RUN apk --no-cache add ca-certificates tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

# Create non-root user and necessary directories
RUN addgroup -S app && adduser -S app -G app && \
    mkdir -p /app/logs /app/config && \
    chown -R app:app /app

WORKDIR /app

# Copy binary file
COPY --from=builder /build/app .

# Copy configuration file template (optional)
# COPY --from=builder /build/config ./config

# Switch to non-root user
USER app

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# Start
ENTRYPOINT ["./app"]

9.4 Complete Workflow Summary

# 1. Write code and Dockerfile
# 2. Local development and testing
make dev              # Start development environment
make test             # Run tests

# 3. Build image
make docker-build     # Build production image

# 4. Local verification
make deploy           # Start full environment
make logs             # View logs
curl http://localhost:8080/health  # Test API

# 5. Push image
make docker-push      # Push to registry

# 6. Deploy to server
ssh your-server
docker pull registry.example.com/myns/myapp:v1.0
docker-compose up -d

# 7. Cleanup
make clean

X. Common Issues and Troubleshooting

10.1 Common Issues

Issue Reason Solution
Image size too large Multi-stage build not used Use multi-stage build
Slow build Cache miss COPY go.mod first, then COPY .
Incorrect timezone in container Missing timezone data Install tzdata, set TZ
HTTPS request failed Missing CA certificates apk add ca-certificates
Cannot connect to other containers Network issue Ensure they are in the same docker network
Data loss Volume not used Use named volume for important data

10.2 Debugging Tips

# View container's internal file system
docker exec -it myapp sh

# View container network configuration
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp

# View container environment variables
docker exec myapp env

# Temporarily run a debug container (sharing network with target container)
docker run -it --rm --network container:myapp alpine sh

# View Docker build process (detailed output)
docker build --progress=plain -t myapp . 2>&1 | tee build.log

Next Article: 016 - Introduction to Kubernetes, learn how to deploy Docker containers to a K8s cluster for production-grade orchestration and management.

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6781

(0)
Walker的头像Walker
上一篇 12 hours ago
下一篇 Mar 8, 2025 12:51

Related Posts

EN
简体中文 繁體中文 English