前言
在现代云原生应用开发中,Go 语言因其高性能、简洁的语法和静态编译的特性而备受青睐。而 Docker 则提供了无与伦比的环境一致性和部署便利性。当我们将这两者结合时,如何高效地管理构建镜像、运行容器、查看日志等一系列操作,就成了一个值得探讨的话题。
直接在终端中手敲 docker build ...
和 docker run ...
等命令,不仅繁琐,而且当参数增多时(如网络配置、环境变量、端口映射),极易出错。这正是 Makefile
发挥其魔力的舞台。通过将这些复杂的 Docker 命令封装成简单的 make
目标,我们可以为团队提供一个统一、简洁、不易出错的开发和部署接口。
本文将以我之前写的一个 pgsql-mcp-server 服务为例,详细解析一个专为 Docker 工作流设计的 Makefile
,展示如何实现一键构建、运行、停止和清理你的 Go 应用容器。
项目准备
按照如下的命令即可拉取最新代码
bash
git clone https://github.com/leixiaotian1/pgsql-mcp-server.git
cd pgsql-mcp-server
项目结构:
go
pgsql-mcp-server/
├── Dockerfile
├── main.go
├── go.mod
├── .env
...
└── Makefile
1. main.go
文件
main文件中主要是读取.env
中的环境变量,连接数据库,选择mcp的通信方式,具体代码如下:
go
package main
import (
"database/sql"
"fmt"
"log"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"github.com/mark3labs/mcp-go/server"
)
type DB struct {
*sql.DB
}
func init() {
if _, err := os.Stat(".env"); err == nil {
if err := godotenv.Load(); err != nil {
log.Fatal("Error loading .env file:", err)
}
}
}
func main() {
// 初始化数据库连接池
if err := initConnectionPool(); err != nil {
log.Fatal("Database connection failed:", err)
}
defer db.Close()
s := server.NewMCPServer(
"pgsql-mcp-server 🚀",
"1.0.0",
)
// Register tools for both server types
s.AddTool(readQueryTool(), readQueryToolHandler)
...省略...
serverMode := os.Getenv("SERVER_MODE")
...省略...
switch serverMode {
case "stdio":
if err := server.ServeStdio(s); err != nil {
log.Printf("Stdio server error: %v\n", err)
}
case "sse":
sse := server.NewSSEServer(s)
log.Printf("sse server listening on :8088/sse")
if err := sse.Start(":8088"); err != nil {
log.Fatalf("Server error: %v", err)
}
case "streamableHttp":
sse := server.NewStreamableHTTPServer(s)
log.Printf("streamableHttp server listening on :8088/mcp")
if err := sse.Start(":8088"); err != nil {
log.Fatalf("Server error: %v", err)
}
default:
log.Fatalf("Unknown SERVER_MODE: %s. Use 'stdio' or 'http'.", serverMode)
}
}
func initConnectionPool() error {
port, err := strconv.Atoi(os.Getenv("DB_PORT"))
if err != nil {
return fmt.Errorf("invalid DB_PORT: %w", err)
}
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
os.Getenv("DB_HOST"),
port,
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"),
os.Getenv("DB_SSLMODE"),
)
db, err = sql.Open("postgres", connStr)
...省略...
if err = db.Ping(); err != nil {
return fmt.Errorf("database ping failed: %w", err)
}
log.Println("Successfully connected to database")
return nil
}
3. .env
文件
存放我们的环境变量,Makefile
将在启动容器时加载它,你在使用的时候记着要自己配置下你的数据库信息以及通信方式。
env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=postgres
DB_USER=admin
DB_PASSWORD=postgres
DB_SSLMODE=disable
SERVER_MODE=sse"
4. Dockerfile
文件
为了构建一个优化的、小体积的 Docker 镜像,我们采用多阶段构建(multi-stage build)。
dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
ENV GO111MODULE=on
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o pgsql-mcp-server .
FROM alpine:latest
COPY --from=builder /app/pgsql-mcp-server /usr/local/bin/pgsql-mcp-server
EXPOSE 8088
CMD ["pgsql-mcp-server"]
Makefile 深度解析
现在,我们来逐行解析文章开头提供的 Makefile
,它是我们整个工作流的核心。
makefile
APP_NAME=pgsql-mcp-server
APP_PORT=8088
DOCKER_IMAGE=$(APP_NAME):latest
DOCKER_CONTAINER=$(APP_NAME)-container
DOCKER_NETWORK=pgsql-mcp-server-network
ENV_FILE=.env
DB_HOST=postgres-shanekAI
.PHONY: build run stop logs clean
## 构建 Docker 镜像
build:
docker build -t $(DOCKER_IMAGE) .
## 运行容器(加载 .env 文件,映射端口)
run: stop
-@docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || docker network create $(DOCKER_NETWORK)
docker network connect $(DOCKER_NETWORK) $(DB_HOST)
docker run -d \
--network=$(DOCKER_NETWORK) \
--name $(DOCKER_CONTAINER) \
--env-file $(ENV_FILE) \
-p $(APP_PORT):$(APP_PORT) \
$(DOCKER_IMAGE)
## 停止并删除容器
stop:
-@docker stop $(DOCKER_CONTAINER) >/dev/null 2>&1 || true
-@docker rm $(DOCKER_CONTAINER) >/dev/null 2>&1 || true
## 查看容器日志
logs:
docker logs -f $(DOCKER_CONTAINER)
## 清理镜像和容器
clean: stop
-@docker rmi $(DOCKER_IMAGE) >/dev/null 2>&1 || true
1. 变量定义
makefile
APP_NAME=pgsql-mcp-server
APP_PORT=8088
DOCKER_IMAGE=$(APP_NAME):latest
DOCKER_CONTAINER=$(APP_NAME)-container
DOCKER_NETWORK=my-app-network
ENV_FILE=.env
将所有可配置的项定义为变量是 Makefile
的最佳实践。这样做的好处是:
- 易于修改:当应用名称或端口改变时,只需修改一处。
- 可读性高 :
$(DOCKER_CONTAINER)
比硬编码的pgsql-mcp-server-container
更清晰。 - 可覆盖 :可以在命令行临时覆盖这些值,例如
make APP_PORT=9000 run
。
2. build
目标
makefile
build:
docker build -t $(DOCKER_IMAGE) .
这个目标非常直观,它执行 docker build
命令,使用当前目录的 Dockerfile
构建镜像,并使用 $(DOCKER_IMAGE)
变量为其打上标签(tag),例如 pgsql-mcp-server:latest
。
3. run
和 stop
目标
makefile
## 运行容器(加载 .env 文件,映射端口)
run: stop
-@docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || docker network create $(DOCKER_NETWORK)
docker network connect $(DOCKER_NETWORK) $(DB_HOST)
docker run -d \
--network=$(DOCKER_NETWORK) \
--name $(DOCKER_CONTAINER) \
--env-file $(ENV_FILE) \
-p $(APP_PORT):$(APP_PORT) \
$(DOCKER_IMAGE)
## 停止并删除容器
stop:
-@docker stop $(DOCKER_CONTAINER) >/dev/null 2>&1 || true
-@docker rm $(DOCKER_CONTAINER) >/dev/null 2>&1 || true
这是整个工作流中最精妙的部分:
run: stop
:这定义了run
目标依赖于stop
目标。意味着每次执行make run
时,make
会先自动执行make stop
。这个模式确保了我们总是在一个干净的状态下启动新的容器,避免了因容器已存在而导致的启动失败。docker network
: 由于我的服务和postgres
在同一个docker
环境中,因此只要容器在同一自定义网络中,Docker
自带的DNS
就能解析容器名,无需额外"桥接",否则就会出现no such host
报错了。docker run
参数详解 :-d
: 后台(detached)模式运行。--network
: 将容器连接到指定的网络,方便未来多容器通信。--name
: 为容器指定一个固定的名字,方便后续通过名字操作它。--env-file
: 加载.env
文件中的所有变量到容器中。-p
: 将主机的端口$(APP_PORT)
映射到容器的$(APP_PORT)
端口。
4. logs
和 clean
目标
makefile
logs:
docker logs -f $(DOCKER_CONTAINER)
clean: stop
-@docker rmi $(DOCKER_IMAGE) >/dev/null 2>&1 || true
logs
:一个便捷的快捷方式,用于实时跟踪(-f
)指定容器的日志。clean
:同样依赖stop
来先清理容器,然后尝试删除 Docker 镜像。错误抑制技巧与stop
目标中的完全相同。
5. 安装示例

实践:完整的工作流
拥有了这个 Makefile
后,我们的日常开发流程变得异常简单:
-
构建 Docker 镜像:
shmake build
-
启动服务容器:
shmake run
执行后,你可以通过
docker ps
看到一个名为pgsql-mcp-server-container
的容器正在运行。 -
查看实时日志:
shmake logs
你会看到 Go 应用的启动日志。按
Ctrl+C
退出日志跟踪。 -
停止并移除容器: 当你需要停止服务时,只需:
shmake stop
-
彻底清理: 如果你想删除容器和镜像,为下一次全新构建做准备:
shmake clean
总结
通过一个看似简单的 Makefile
,我们成功地将一系列复杂的 Docker 命令抽象成了几个简单、符合直觉的指令。这种方法带来了诸多好处:
- 效率提升 :用
make run
替代冗长的docker run ...
命令。 - 降低心智负担:开发者无需记住所有 Docker 参数和容器名称。
- 团队协作:为所有团队成员提供了统一的操作标准,降低了新成员上手的门槛。
- CI/CD 集成 :这些
make
命令可以被无缝地集成到 Jenkins、GitLab CI 或 GitHub Actions 等自动化流程中。