Docker 部署:让你的应用随处运行

学习如何将 Go 应用容器化,使用 Docker 和 Docker Compose 进行部署

Docker 部署:让你的应用随处运行

“在我的机器上能跑啊!"——这大概是开发者说过最多的一句话了。

不同的开发环境、不同的依赖版本、不同的操作系统……这些问题让应用的部署变得复杂而痛苦。Docker 的出现彻底改变了这一局面:把你的应用和它的所有依赖打包成一个容器,在任何地方都能以相同的方式运行。

Go 语言和 Docker 是天作之合。Go 编译出的静态二进制文件非常小,而且不需要运行时依赖,这让 Go 的 Docker 镜像可以做得极其精简。

今天我们就来学习如何用 Docker 部署 Go 应用。

Dockerfile 基础

最简单的 Dockerfile

# 使用官方 Go 镜像
FROM golang:1.16

# 设置工作目录
WORKDIR /app

# 复制代码
COPY . .

# 构建
RUN go build -o myapp

# 运行
CMD ["./myapp"]

构建和运行:

docker build -t myapp .
docker run -p 8080:8080 myapp

这个 Dockerfile 很简单,但生成的镜像非常大(约 1GB),因为包含了完整的 Go 工具链。

多阶段构建(推荐)

多阶段构建可以生成更小的镜像:

# 阶段 1:构建
FROM golang:1.16 AS builder

WORKDIR /app

# 先复制 go.mod 和 go.sum,利用 Docker 缓存
COPY go.mod go.sum ./
RUN go mod download

# 复制代码并构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .

# 阶段 2:运行
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# 从构建阶段复制二进制文件
COPY --from=builder /app/myapp .

CMD ["./myapp"]

构建后的镜像只有约 10-20 MB!

极简镜像(scratch)

如果想要更小的镜像,可以使用 scratch(空镜像):

# 构建阶段
FROM golang:1.16 AS builder

WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .

# 运行阶段:使用空镜像
FROM scratch

# 复制 CA 证书(如果需要 HTTPS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 复制二进制文件
COPY --from=builder /app/myapp /myapp

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

EXPOSE 8080

CMD ["/myapp"]

生成的镜像只有你的二进制文件大小(通常几 MB)!

优化 Docker 构建

1. 利用构建缓存

# ❌ 不好:任何文件变化都会导致重新下载依赖
COPY . .
RUN go mod download
RUN go build -o myapp

# ✅ 好:先复制依赖文件,利用缓存
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp

2. 使用 .dockerignore

创建 .dockerignore 文件,排除不需要的文件:

.git
.gitignore
README.md
Dockerfile
docker-compose.yml
.vscode
.idea
*.log
vendor/

3. 并行构建多个平台

# 构建多平台镜像
FROM golang:1.16 AS builder

WORKDIR /app
COPY . .

# 构建 Linux amd64
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 .

# 构建 Linux arm64
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 .

Docker Compose

对于多服务应用,使用 Docker Compose 更方便:

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=password
      - DB_NAME=myapp
      - REDIS_ADDR=redis:6379
    depends_on:
      - db
      - redis
    restart: unless-stopped
    networks:
      - myapp-network

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=myapp
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "3306:3306"
    networks:
      - myapp-network

  redis:
    image: redis:6-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - myapp-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    networks:
      - myapp-network

volumes:
  mysql-data:
  redis-data:

networks:
  myapp-network:
    driver: bridge

启动所有服务:

docker-compose up -d

查看日志:

docker-compose logs -f app

实战:完整的 Web 应用部署

让我们部署一个完整的 Go Web 应用:

应用代码

// main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
	
	_ "github.com/go-sql-driver/mysql"
	"github.com/go-redis/redis/v8"
)

var (
	db    *sql.DB
	rdb   *redis.Client
)

func main() {
	// 连接数据库
	var err error
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASSWORD"),
		os.Getenv("DB_HOST"),
		os.Getenv("DB_PORT"),
		os.Getenv("DB_NAME"),
	)
	
	for i := 0; i < 30; i++ {
		db, err = sql.Open("mysql", dsn)
		if err == nil {
			err = db.Ping()
			if err == nil {
				break
			}
		}
		log.Printf("等待数据库连接... (%d/30)", i+1)
		time.Sleep(2 * time.Second)
	}
	
	if err != nil {
		log.Fatalf("数据库连接失败: %v", err)
	}
	defer db.Close()
	
	// 连接 Redis
	rdb = redis.NewClient(&redis.Options{
		Addr: os.Getenv("REDIS_ADDR"),
	})
	
	if err := rdb.Ping(rdb.Context()).Err(); err != nil {
		log.Fatalf("Redis 连接失败: %v", err)
	}
	
	// HTTP 路由
	http.HandleFunc("/", homeHandler)
	http.HandleFunc("/health", healthHandler)
	
	// 启动服务器
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	
	log.Printf("服务器启动在 :%s", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello from Docker!")
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
	// 检查数据库
	if err := db.Ping(); err != nil {
		http.Error(w, "Database error", http.StatusServiceUnavailable)
		return
	}
	
	// 检查 Redis
	if err := rdb.Ping(rdb.Context()).Err(); err != nil {
		http.Error(w, "Redis error", http.StatusServiceUnavailable)
		return
	}
	
	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "OK")
}

Dockerfile

# 构建阶段
FROM golang:1.16-alpine AS builder

RUN apk add --no-cache git

WORKDIR /app

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

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .

# 运行阶段
FROM alpine:latest

RUN apk --no-cache add ca-certificates tzdata

# 设置时区
ENV TZ=Asia/Shanghai

WORKDIR /app

COPY --from=builder /app/myapp .
COPY --from=builder /app/config.yaml .

# 创建非 root 用户
RUN adduser -D -u 1000 appuser
USER appuser

EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1

CMD ["./myapp"]

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=3306
      - DB_USER=myapp
      - DB_PASSWORD=secret
      - DB_NAME=myapp
      - REDIS_ADDR=redis:6379
      - PORT=8080
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=myapp
      - MYSQL_USER=myapp
      - MYSQL_PASSWORD=secret
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:6-alpine
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes

volumes:
  mysql-data:
  redis-data:

常用命令

# 构建镜像
docker build -t myapp:latest .

# 运行容器
docker run -d -p 8080:8080 --name myapp myapp:latest

# 查看日志
docker logs -f myapp

# 进入容器
docker exec -it myapp sh

# 停止容器
docker stop myapp

# 删除容器
docker rm myapp

# 查看资源使用
docker stats

# 清理未使用的镜像
docker image prune -a

生产环境最佳实践

1. 使用非 root 用户

RUN adduser -D -u 1000 appuser
USER appuser

2. 设置资源限制

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 1G

3. 配置日志

logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

4. 使用 secrets 管理敏感信息

services:
  app:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

小结

今天我们学习了 Go 应用的 Docker 部署:

  1. Dockerfile:基础、多阶段构建、scratch 镜像
  2. 构建优化:缓存、.dockerignore、多平台
  3. Docker Compose:多服务编排
  4. 实战:完整 Web 应用部署
  5. 最佳实践:安全、资源限制、日志、secrets

Docker 让 Go 应用的部署变得简单而可靠。无论是开发、测试还是生产环境,都能以相同的方式运行。

练习时间

  1. CI/CD:集成 GitHub Actions 自动构建和推送镜像
  2. Kubernetes:将应用部署到 K8s 集群
  3. 监控:集成 Prometheus 和 Grafana
  4. 蓝绿部署:实现零停机部署

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页