Skip to content

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

postrm 脚本的清理逻辑与 purge 陷阱示意图

在前面三章中,我们深入理解了 Debian 维护者脚本(postinst、prerm、postrm)的运行机制和最佳实践。本章将把这些理论知识应用到真实的 Golang 项目中,完成从源码到 .deb 包的完整打包流程。

我们将采用 多阶段 Docker 构建方案,实现编译环境与打包环境的分离,确保构建过程可复现、可自动化。

整体方案概述

为什么选择 Docker 多阶段构建?

方案优点缺点
直接在本机打包简单直接环境依赖复杂,不可复现
使用 dh-golangDebian 官方方案学习曲线陡峭,老旧系统兼容性差
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 ./service

systemd 服务配置文件

创建 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 关键点解读

阶段关键步骤说明
Buildergo mod vendor将依赖 vendor 化,确保离线可构建
BuilderCGO_ENABLED=0生成静态二进制,避免 glibc 依赖问题
Packager多阶段 COPY只复制二进制,不复制源码
Packagerdpkg-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 实现自动化部署。


下一篇预告第五章:VUE 项目打包为 Debian 包与 Nginx 自动配置

最后更新2026/07/25 14:21
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal