English
Complete Guide to Packaging a Golang Project as a Debian Package

In the previous three chapters, we gained an in-depth understanding of the operating mechanisms and best practices of Debian maintainer scripts (postinst, prerm, postrm). This chapter will apply this theoretical knowledge to Real Golang project in, complete from source code to .deb The complete packaging process for the package.
We will adopt Multi-stage Docker build Solution: separate the compilation environment from the packaging environment to ensure the build process is reproducible and automatable.
Overall Solution Overview
Why choose Docker multi-stage builds?
| Plan | Advantages | Shortcoming |
|---|---|---|
| Package directly on the local machine. | Simple and direct | Complex environment dependencies, not reproducible. |
use dh-golang | Debian official solution | Steep learning curve and poor compatibility with legacy systems. |
| Docker multi-stage build | Environment isolation, reproducible, CI/CD-friendly | Docker environment required |
This solution uses Docker multi-stage builds, dividing compilation and packaging into two independent stages, ultimately outputting a standard .deb Bag.
Final deliverable
out/debian/apiserver-1.7.0-amd64.debthis .deb Package includes:
✅ Compiled Golang binary
✅ systemd service definition file
✅ Default configuration file
✅ postinst / prerm / postrm maintainer scripts
Project directory structure
Before we begin, first clarify the final project directory structure:
text
<appname>/
├── cmd/
│ └── <appname>/
│ ├── main.go
│ └── etc/
│ └── config.yaml # Application configuration file
├── docker/
│ └── debian/
│ ├── builder.Dockerfile # Docker build file
│ ├── etc/
│ │ └── config.yaml -> ../../../cmd/<appname>/etc/config.yaml
│ ├── scripts/
│ │ ├── postinst # post-install script
│ │ ├── prerm # Pre-uninstall script
│ │ └── postrm # Post-uninstall script
│ └── service/ # -> ../../supervisor/linux (soft link)
│ └── <appname>.service # systemd service definition
├── supervisor/
│ └── linux/
│ └── <appname>.service # systemd service definition source file
├── .dockerignore
└── Makefile📌naming convention: This paper uses apiserver As an example service name, you can replace it with the actual project name.
Step 1: Create the directory structure
bash
# Create Debian packaging build directory
mkdir -p docker/debian
# Enter directory
cd docker/debianStep 2: Configure the systemd service definition
Create a service definition directory (soft link)
bash
# Execute in the docker/debian directory
ln -s ../../supervisor/linux ./servicesystemd service configuration file
Create 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💡Hint:Alias=app.service Allow you to use systemctl status app to manage services and enhance the operating experience.
⚠️Important: all appname Replace with your actual service name.
Step 3: Configuration File Management
Create the configuration directory and link the configuration file.
bash
# Create configuration directory
mkdir -p docker/debian/etc
# Enter the configuration directory
cd docker/debian/etc
# Create a symbolic link for the configuration file (pointing to the project's actual configuration file)
ln -s ../../../cmd/apiserver/etc/config.yaml .Configuration file handling strategy
At postinst In the script, we will handle the logic for first-time installation and upgrade preservation of configuration files:
bash
# First installation: copy from template
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📌Design principles: Maintain a single source of configuration and use symbolic links to avoid maintenance chaos caused by multiple copies.
Step 4: Maintainer script
postinst (post-installation script)
bash
#!/bin/sh
set -e
case "$1" in
configure)
# 1. Create a system service user (optional)
if ! id appname >/dev/null 2>&1; then
useradd -r -s /usr/sbin/nologin -d /var/lib/appname appname
fi
# 2. Create a directory and set permissions
mkdir -p /var/{log/appname,lib/appname}
chmod 755 /var/{log/appname,lib/appname}
chown -R appname:appname /var/{log/appname,lib/appname}
# 3. Handle configuration files (first installation vs upgrade)
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. Enable the systemd service
systemctl enable appname.service
systemctl daemon-reload
systemctl restart appname.service
;;
esac
exit 0📖Deep understanding: For more details about postinst, please refer to Chapter 1: Complete Interpretation and Best Practices of postinst Scripts
prerm (pre-removal script)
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📖Deep understanding: For more details about prerm, please refer to Chapter 2: The Role of the prerm Script and Correct Usage Practices
postrm (post-removal script)
bash
#!/bin/sh
set -e
case "$1" in
purge)
# Delete the data directory and log directory.
[ -d /var/lib/apiserver ] && rm -rf /var/lib/apiserver
[ -d /var/log/apiserver ] && rm -rf /var/log/apiserver
# Clean up systemd leftovers
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)
# Only clean up systemd, keep the data.
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📖Deep understanding: For more details about postrm, please refer to Chapter 3: Cleanup Logic of the postrm Script and the purge Trap
Step 5: Docker build file (builder.Dockerfile)
This is the entire packaging process's Core files, using multi-stage builds to separate compilation from packaging.
Complete Dockerfile
dockerfile
# ============================================================
# Global build parameters
# ============================================================
ARG VERSION
ARG APPNAME=apiserver
ARG GOPRIVATE=github.com/channellabs,github.com/go-connecter
# ============================================================
# Stage 1: Go build environment
# ============================================================
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 configuration (for pulling private repositories)
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 the source code and build
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 packaging environment
# ============================================================
FROM ubuntu:20.04
ARG VERSION
ARG APPNAME
ENV VERSION=$VERSION
ENV APPNAME=$APPNAME
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
# Configure domestic mirror source
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
# Configure time zone
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
# Install packaging tool
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 binary file
COPY --from=builder /${APPNAME} /usr/bin/${APPNAME}
# Create system directory structure
RUN mkdir -p \
/etc/${APPNAME} \
/var/log/${APPNAME} \
/var/lib/${APPNAME}
# Copy configuration file
COPY docker/debian/etc/config.yaml /etc/${APPNAME}/${APPNAME}.yaml
RUN chmod 644 /etc/${APPNAME}/${APPNAME}.yaml
# Copy the systemd service definition
COPY docker/debian/service/${APPNAME}.service /lib/systemd/system/${APPNAME}.service
RUN chmod 644 /lib/systemd/system/${APPNAME}.service
# Copy maintainer script
COPY docker/debian/scripts/postinst docker/debian/scripts/postrm docker/debian/scripts/prerm /tmp/
RUN chmod +x /tmp/postinst /tmp/postrm /tmp/prerm
# ============================================================
# Build the Debian package directory structure
# ============================================================
RUN mkdir -p package && \
# binary file
mkdir -p package/usr/bin && \
cp /usr/bin/${APPNAME} package/usr/bin/ && \
# Configuration file
mkdir -p package/etc/${APPNAME} && \
cp /etc/${APPNAME}/${APPNAME}.yaml package/etc/${APPNAME}/ && \
# systemd service
mkdir -p package/lib/systemd/system && \
cp /lib/systemd/system/${APPNAME}.service package/lib/systemd/system/ && \
# Maintainer script
mkdir -p package/DEBIAN && \
mv /tmp/postinst package/DEBIAN/ && \
mv /tmp/postrm package/DEBIAN/ && \
mv /tmp/prerm package/DEBIAN/ && \
# control file
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
# Package
RUN dpkg-deb --build package && \
mv package.deb /${APPNAME}-${VERSION}-amd64.deb
# Output
VOLUME /workspace
CMD ["sh", "-c", "cp /${APPNAME}-${VERSION}-amd64.deb /output/"]Dockerfile Key Points Explained
| Stage | Key steps | Description |
|---|---|---|
| Builder | go mod vendor | Vendor the dependencies to ensure offline buildability. |
| Builder | CGO_ENABLED=0 | Generate static binaries to avoid glibc dependency issues |
| Packager | Multi-stage COPY | Copy only the binary, not the source code. |
| Packager | dpkg-deb --build | Generate a standard .deb package |
Step 6: .dockerignore
Create in the project root directory .dockerignore, exclude unnecessary files:
txt
.git
*.md
tmp/
vendor
out
buildStep 7: Makefile automated build
makefile
OUTPUT_DIR ?= ./out
.PHONY: builder-debian
# Build Debian package
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"Execute build
bash
make builder-debian VERSION=1.7.0After the build is complete,.deb The package will be output to out/debian/apiserver-1.7.0-amd64.deb.
Step 8: Deployment and Verification
Upload to the server
bash
scp out/debian/apiserver-1.7.0-amd64.deb username@host:/tmp/💡Prompt: It is recommended to place the .deb package in /tmp/ Install in the directory to avoid dpkg: warning: installing as root Warning.
Install software packages
bash
# Use apt (recommended, automatically handles dependencies)
sudo apt install /tmp/apiserver-1.7.0-amd64.deb
# Or use dpkg
sudo dpkg -i /tmp/apiserver-1.7.0-amd64.debVerify installation
bash
# View package information
dpkg -l apiserver
# Check service status
systemctl status apiserver
# View logs
tail -f /var/log/apiserver/apiserver.logCommon dpkg command quick reference
| Command | Use |
|---|---|
dpkg -I <package.deb> | View package metadata |
dpkg -c <package.deb> | View package content list |
dpkg -i <package.deb> | Install software package |
dpkg -r <package> | Uninstall (keep configuration) |
dpkg -P <package> | Completely eliminate |
dpkg -l <package> | View installation status |
Common troubleshooting
Problem 1: Prompt during installation has no Size information
This is a warning caused by APT detecting incomplete local package metadata, and it does not affect functionality. For the solution, refer to the relevant documentation.
Problem 2: The systemd service fails to start
bash
# View error details
systemctl status apiserver -l
# Check logs
journalctl -u apiserver -n 50Problem 3: Configuration file overwritten
Check postinst whether it is used in cp -f Force overwrite. The correct approach is:
bash
if [ ! -f /etc/appname/appname.yaml ]; then
cp /etc/appname/appname.yaml.template /etc/appname/appname.yaml
fiSummary of Pros and Cons of the Plan
| advantages | Shortcoming |
|---|---|
| ✅ Environment isolation, reproducible builds | ❌ Docker environment required |
| ✅ Supports private repositories (SSH) | ❌ Large image size (can be optimized) |
| ✅ CI/CD friendly | |
| ✅ Standard .deb output | |
| ✅ Complete lifecycle management |
Next step
In the next chapter, we will package the front-end Vue project into a Debian package and implement automated deployment with Nginx.
Next post preview:Chapter 5: Packaging a VUE Project into a Debian Package and Automatic Nginx Configuration
