使用 Makefile 和 Docker 简化你的 Go 服务部署流程

前言

在现代云原生应用开发中,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. runstop 目标

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. logsclean 目标

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 后,我们的日常开发流程变得异常简单:

  1. 构建 Docker 镜像:

    sh 复制代码
    make build
  2. 启动服务容器:

    sh 复制代码
    make run

    执行后,你可以通过 docker ps看到一个名为 pgsql-mcp-server-container 的容器正在运行。

  3. 查看实时日志:

    sh 复制代码
    make logs

    你会看到 Go 应用的启动日志。按 Ctrl+C 退出日志跟踪。

  4. 停止并移除容器: 当你需要停止服务时,只需:

    sh 复制代码
    make stop
  5. 彻底清理: 如果你想删除容器和镜像,为下一次全新构建做准备:

    sh 复制代码
    make clean

总结

通过一个看似简单的 Makefile,我们成功地将一系列复杂的 Docker 命令抽象成了几个简单、符合直觉的指令。这种方法带来了诸多好处:

  • 效率提升 :用 make run 替代冗长的 docker run ... 命令。
  • 降低心智负担:开发者无需记住所有 Docker 参数和容器名称。
  • 团队协作:为所有团队成员提供了统一的操作标准,降低了新成员上手的门槛。
  • CI/CD 集成 :这些 make 命令可以被无缝地集成到 Jenkins、GitLab CI 或 GitHub Actions 等自动化流程中。
相关推荐
就是帅我不改1 分钟前
告别996!高可用低耦合架构揭秘:SpringBoot + RabbitMQ 让订单系统不再崩
java·后端·面试
用户61204149221315 分钟前
C语言做的区块链模拟系统(极简版)
c语言·后端·敏捷开发
Mintopia25 分钟前
🎬《Next 全栈 CRUD 的百老汇》
前端·后端·next.js
CF14年老兵1 小时前
深入浅出 Python 一等函数:一份友好的全面解析
后端·python·trae
whitepure1 小时前
万字详解常用数据结构(Java版)
java·数据结构·后端
天天摸鱼的java工程师1 小时前
你们公司的 QPS 是怎么统计出来的?这 5 种常见方法我踩过一半的坑
java·后端·面试
guojl1 小时前
Gateway使用手册
后端·微服务
BingoGo1 小时前
PHP 内存管理 深入理解 PHP 的引用和垃圾回收
后端·php
whitepure1 小时前
万字详解常用算法(Java版)
java·后端·算法
程序员NEO1 小时前
Spring 调试新姿势:一眼看清运行时,用 Spring Debugger 少踩 90% 坑
后端