Skip to content

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

Schematic diagram of the postrm script's cleanup logic and purge pitfalls

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? ​

PlanAdvantagesShortcoming
Package directly on the local machine.Simple and directComplex environment dependencies, not reproducible.
use dh-golangDebian official solutionSteep learning curve and poor compatibility with legacy systems.
Docker multi-stage buildEnvironment isolation, reproducible, CI/CD-friendlyDocker 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.deb

this .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/debian

Step 2: Configure the systemd service definition ​

bash
# Execute in the docker/debian directory
ln -s ../../supervisor/linux ./service

systemd 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 ​

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 ​

StageKey stepsDescription
Buildergo mod vendorVendor the dependencies to ensure offline buildability.
BuilderCGO_ENABLED=0Generate static binaries to avoid glibc dependency issues
PackagerMulti-stage COPYCopy only the binary, not the source code.
Packagerdpkg-deb --buildGenerate a standard .deb package

Step 6: .dockerignore ​

Create in the project root directory .dockerignore, exclude unnecessary files:

txt
.git
*.md
tmp/
vendor
out
build

Step 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.0

After 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.deb

Verify installation ​

bash
# View package information
dpkg -l apiserver

# Check service status
systemctl status apiserver

# View logs
tail -f /var/log/apiserver/apiserver.log

Common dpkg command quick reference ​

CommandUse
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 50

Problem 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
fi

Summary of Pros and Cons of the Plan ​

advantagesShortcoming
✅ 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

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