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 部署:
- Dockerfile:基础、多阶段构建、scratch 镜像
- 构建优化:缓存、.dockerignore、多平台
- Docker Compose:多服务编排
- 实战:完整 Web 应用部署
- 最佳实践:安全、资源限制、日志、secrets
Docker 让 Go 应用的部署变得简单而可靠。无论是开发、测试还是生产环境,都能以相同的方式运行。
练习时间
- CI/CD:集成 GitHub Actions 自动构建和推送镜像
- Kubernetes:将应用部署到 K8s 集群
- 监控:集成 Prometheus 和 Grafana
- 蓝绿部署:实现零停机部署
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。