Appearance
Golang 项目打包为 Debian 包完整指南

在前面三章中,我们深入理解了 Debian 维护者脚本(postinst、prerm、postrm)的运行机制和最佳实践。本章将把这些理论知识应用到真实的 Golang 项目中,完成从源码到 .deb 包的完整打包流程。
我们将采用 多阶段 Docker 构建方案,实现编译环境与打包环境的分离,确保构建过程可复现、可自动化。
整体方案概述
为什么选择 Docker 多阶段构建?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 直接在本机打包 | 简单直接 | 环境依赖复杂,不可复现 |
使用 dh-golang | Debian 官方方案 | 学习曲线陡峭,老旧系统兼容性差 |
| Docker 多阶段构建 | 环境隔离、可复现、CI/CD 友好 | 需要 Docker 环境 |
本方案采用 Docker 多阶段构建,将编译和打包分为两个独立的阶段,最终输出一个标准的 .deb 包。
最终交付物
out/debian/apiserver-1.7.0-amd64.deb这个 .deb 包包含:
- ✅ 编译好的 Golang 二进制文件
- ✅ systemd 服务定义文件
- ✅ 默认配置文件
- ✅ postinst / prerm / postrm 维护者脚本
项目目录结构
在开始之前,先明确最终的项目目录结构:
text
<appname>/
├── cmd/
│ └── <appname>/
│ ├── main.go
│ └── etc/
│ └── config.yaml # 应用配置文件
├── docker/
│ └── debian/
│ ├── builder.Dockerfile # Docker 构建文件
│ ├── etc/
│ │ └── config.yaml -> ../../../cmd/<appname>/etc/config.yaml
│ ├── scripts/
│ │ ├── postinst # 安装后脚本
│ │ ├── prerm # 卸载前脚本
│ │ └── postrm # 卸载后脚本
│ └── service/ # -> ../../supervisor/linux (软链接)
│ └── <appname>.service # systemd 服务定义
├── supervisor/
│ └── linux/
│ └── <appname>.service # systemd 服务定义源文件
├── .dockerignore
└── Makefile📌 命名约定:本文以
apiserver作为服务名称示例,你可以替换为实际的项目名称。
第一步:创建目录结构
bash
# 创建 Debian 打包构建目录
mkdir -p docker/debian
# 进入目录
cd docker/debian第二步:配置 systemd 服务定义
创建服务定义目录(软链接)
bash
# 在 docker/debian 目录下执行
ln -s ../../supervisor/linux ./servicesystemd 服务配置文件
创建 supervisor/linux/<appname>.service:
systemd
[Unit]
Description="AppName - Standardized Golang project build service"
Requires=network-online.target
After=network-online.target
[Service]
WorkingDirectory=/var/lib/appname
StandardOutput=append:/var/log/appname/appname.log
StandardError=append:/var/log/appname/appname.log
User=root
Group=root
ExecStart=/usr/bin/appname --config-dir /etc/appname --config-name appname.yaml
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGTERM
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Alias=app.service💡 提示:
Alias=app.service允许你使用systemctl status app来管理服务,提升操作体验。
⚠️ 重要:将所有
appname替换为你的实际服务名称。
第三步:配置文件管理
创建配置目录并链接配置文件
bash
# 创建配置目录
mkdir -p docker/debian/etc
# 进入配置目录
cd docker/debian/etc
# 创建配置文件软链接(指向项目实际配置文件)
ln -s ../../../cmd/apiserver/etc/config.yaml .配置文件处理策略
在 postinst 脚本中,我们会处理配置文件的首次安装和升级保留逻辑:
bash
# 首次安装:从模板复制
if [ ! -f /etc/appname/appname.yaml ]; then
cp /etc/appname/appname.yaml.template /etc/appname/appname.yaml
chown appname:appname /etc/appname/appname.yaml
fi📌 设计原则:保持单一配置源,通过软链接避免多份拷贝导致的维护混乱。
第四步:维护者脚本
postinst(安装后脚本)
bash
#!/bin/sh
set -e
case "$1" in
configure)
# 1. 创建系统服务用户(可选)
if ! id appname >/dev/null 2>&1; then
useradd -r -s /usr/sbin/nologin -d /var/lib/appname appname
fi
# 2. 创建目录并设置权限
mkdir -p /var/{log/appname,lib/appname}
chmod 755 /var/{log/appname,lib/appname}
chown -R appname:appname /var/{log/appname,lib/appname}
# 3. 处理配置文件(首次安装 vs 升级)
if [ ! -f /etc/appname/appname.yaml ]; then
cp /etc/appname/appname.yaml.template /etc/appname/appname.yaml
chown appname:appname /etc/appname/appname.yaml
fi
# 4. 启用 systemd 服务
systemctl enable appname.service
systemctl daemon-reload
systemctl restart appname.service
;;
esac
exit 0📖 深入理解:关于 postinst 的更多细节,请参考 第一章:postinst 脚本完整解读与最佳实践
prerm(卸载前脚本)
bash
#!/bin/sh
set -e
case "$1" in
remove|purge)
echo "Stopping service before uninstall..." >&2
if command -v systemctl >/dev/null 2>&1; then
systemctl stop apiserver.service 2>/dev/null || true
systemctl disable apiserver.service 2>/dev/null || true
fi
;;
upgrade)
if command -v systemctl >/dev/null 2>&1; then
systemctl stop apiserver.service 2>/dev/null || true
fi
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0📖 深入理解:关于 prerm 的更多细节,请参考 第二章:prerm 脚本的作用与正确使用姿势
postrm(卸载后脚本)
bash
#!/bin/sh
set -e
case "$1" in
purge)
# 删除数据目录和日志目录
[ -d /var/lib/apiserver ] && rm -rf /var/lib/apiserver
[ -d /var/log/apiserver ] && rm -rf /var/log/apiserver
# 清理 systemd 残留
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null || true
systemctl reset-failed apiserver.service 2>/dev/null || true
fi
;;
remove)
# 仅清理 systemd,保留数据
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null || true
systemctl reset-failed apiserver.service 2>/dev/null || true
fi
;;
esac
exit 0📖 深入理解:关于 postrm 的更多细节,请参考 第三章:postrm 脚本的清理逻辑与 purge 陷阱
第五步:Docker 构建文件(builder.Dockerfile)
这是整个打包流程的核心文件,采用多阶段构建实现编译与打包分离。
完整 Dockerfile
dockerfile
# ============================================================
# 全局构建参数
# ============================================================
ARG VERSION
ARG APPNAME=apiserver
ARG GOPRIVATE=github.com/channellabs,github.com/go-connecter
# ============================================================
# Stage 1: Go 编译环境
# ============================================================
FROM golang:1.23 AS builder
ARG VERSION
ARG APPNAME
ARG GOPRIVATE
ENV VERSION=$VERSION
ENV GOPROXY=https://goproxy.cn,direct
ENV GOPRIVATE=${GOPRIVATE}
WORKDIR /app
# SSH 配置(用于拉取私有仓库)
RUN --mount=type=ssh \
git config --global url."[email protected]:".insteadOf "https://github.com/" && \
mkdir -p ~/.ssh && \
ssh-keyscan github.com >> ~/.ssh/known_hosts
# 复制源码并构建
COPY . .
RUN --mount=type=ssh go mod tidy && go mod vendor
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -o /${APPNAME} cmd/${APPNAME}/main.go
# ============================================================
# Stage 2: Debian 打包环境
# ============================================================
FROM ubuntu:20.04
ARG VERSION
ARG APPNAME
ENV VERSION=$VERSION
ENV APPNAME=$APPNAME
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
# 配置国内镜像源
RUN sed -i \
-e 's|http://.*archive.ubuntu.com|http://mirrors.aliyun.com|g' \
-e 's|http://.*security.ubuntu.com|http://mirrors.aliyun.com|g' \
/etc/apt/sources.list
# 配置时区
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata && \
ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure --frontend noninteractive tzdata
# 安装打包工具
RUN apt-get update && \
apt-get install -y --no-install-recommends \
dpkg-dev \
ca-certificates \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
# 复制二进制文件
COPY --from=builder /${APPNAME} /usr/bin/${APPNAME}
# 创建系统目录结构
RUN mkdir -p \
/etc/${APPNAME} \
/var/log/${APPNAME} \
/var/lib/${APPNAME}
# 复制配置文件
COPY docker/debian/etc/config.yaml /etc/${APPNAME}/${APPNAME}.yaml
RUN chmod 644 /etc/${APPNAME}/${APPNAME}.yaml
# 复制 systemd 服务定义
COPY docker/debian/service/${APPNAME}.service /lib/systemd/system/${APPNAME}.service
RUN chmod 644 /lib/systemd/system/${APPNAME}.service
# 复制维护者脚本
COPY docker/debian/scripts/postinst docker/debian/scripts/postrm docker/debian/scripts/prerm /tmp/
RUN chmod +x /tmp/postinst /tmp/postrm /tmp/prerm
# ============================================================
# 构建 Debian 包目录结构
# ============================================================
RUN mkdir -p package && \
# 二进制文件
mkdir -p package/usr/bin && \
cp /usr/bin/${APPNAME} package/usr/bin/ && \
# 配置文件
mkdir -p package/etc/${APPNAME} && \
cp /etc/${APPNAME}/${APPNAME}.yaml package/etc/${APPNAME}/ && \
# systemd 服务
mkdir -p package/lib/systemd/system && \
cp /lib/systemd/system/${APPNAME}.service package/lib/systemd/system/ && \
# 维护者脚本
mkdir -p package/DEBIAN && \
mv /tmp/postinst package/DEBIAN/ && \
mv /tmp/postrm package/DEBIAN/ && \
mv /tmp/prerm package/DEBIAN/ && \
# control 文件
cat > package/DEBIAN/control <<EOF
Package: ${APPNAME}
Version: ${VERSION}
Architecture: amd64
Maintainer: Applyfly <[email protected]>
Description: A ${APPNAME} Golang Debian package
Installed-Size: $(du -s package/usr/ | cut -f1)
Size: $(du -s package | cut -f1)
Depends: systemd
Pre-Depends: debconf
Section: utils
Priority: optional
EOF
# 打包
RUN dpkg-deb --build package && \
mv package.deb /${APPNAME}-${VERSION}-amd64.deb
# 输出
VOLUME /workspace
CMD ["sh", "-c", "cp /${APPNAME}-${VERSION}-amd64.deb /output/"]Dockerfile 关键点解读
| 阶段 | 关键步骤 | 说明 |
|---|---|---|
| Builder | go mod vendor | 将依赖 vendor 化,确保离线可构建 |
| Builder | CGO_ENABLED=0 | 生成静态二进制,避免 glibc 依赖问题 |
| Packager | 多阶段 COPY | 只复制二进制,不复制源码 |
| Packager | dpkg-deb --build | 生成标准的 .deb 包 |
第六步:.dockerignore
在项目根目录创建 .dockerignore,排除不必要的文件:
txt
.git
*.md
tmp/
vendor
out
build第七步:Makefile 自动化构建
makefile
OUTPUT_DIR ?= ./out
.PHONY: builder-debian
# 构建 Debian 包
builder-debian:
ifndef VERSION
$(error VERSION is required. Usage: make builder-debian VERSION=1.7.0)
endif
@echo "Building Debian package..."
@eval "$(ssh-agent -s)"
@ssh-add ~/.ssh/id_rsa
@mkdir -p $(OUTPUT_DIR)/debian
DOCKER_BUILDKIT=1 docker build \
--ssh default \
-f docker/debian/builder.Dockerfile \
-t apiserver-builder:$(VERSION) \
--build-arg VERSION=$(VERSION) \
.
@docker run --rm -v $(abspath $(OUTPUT_DIR)/debian):/output apiserver-builder:$(VERSION)
@echo "Package built: $(OUTPUT_DIR)/debian/apiserver-$(VERSION)-amd64.deb"执行构建
bash
make builder-debian VERSION=1.7.0构建完成后,.deb 包会输出到 out/debian/apiserver-1.7.0-amd64.deb。
第八步:部署与验证
上传到服务器
bash
scp out/debian/apiserver-1.7.0-amd64.deb username@host:/tmp/💡 提示:建议将 .deb 包放在
/tmp/目录下安装,避免dpkg: warning: installing as root警告。
安装软件包
bash
# 使用 apt(推荐,会自动处理依赖)
sudo apt install /tmp/apiserver-1.7.0-amd64.deb
# 或使用 dpkg
sudo dpkg -i /tmp/apiserver-1.7.0-amd64.deb验证安装
bash
# 查看包信息
dpkg -l apiserver
# 查看服务状态
systemctl status apiserver
# 查看日志
tail -f /var/log/apiserver/apiserver.log常用 dpkg 命令速查
| 命令 | 用途 |
|---|---|
dpkg -I <package.deb> | 查看包元信息 |
dpkg -c <package.deb> | 查看包内容列表 |
dpkg -i <package.deb> | 安装软件包 |
dpkg -r <package> | 卸载(保留配置) |
dpkg -P <package> | 彻底清除 |
dpkg -l <package> | 查看安装状态 |
常见问题排查
问题1:安装时提示 has no Size information
这是 APT 检测到本地包元数据不完整导致的警告,不影响功能。解决方式参考相关文档。
问题2:systemd 服务无法启动
bash
# 查看详细错误
systemctl status apiserver -l
# 检查日志
journalctl -u apiserver -n 50问题3:配置文件被覆盖
检查 postinst 中是否使用了 cp -f 强制覆盖。正确的做法是:
bash
if [ ! -f /etc/appname/appname.yaml ]; then
cp /etc/appname/appname.yaml.template /etc/appname/appname.yaml
fi方案优缺点总结
| 优点 | 缺点 |
|---|---|
| ✅ 环境隔离,构建可复现 | ❌ 需要 Docker 环境 |
| ✅ 支持私有仓库(SSH) | ❌ 镜像体积较大(可优化) |
| ✅ CI/CD 友好 | |
| ✅ 标准 .deb 输出 | |
| ✅ 完整的生命周期管理 |
下一步
在下一章中,我们将把前端 VUE 项目打包为 Debian 包,与 Nginx 实现自动化部署。
