Building a Production-Ready APT Package and Repository with Nexus: A Practical Deep Dive

Introduction

This article walks through the complete lifecycle of building, packaging, publishing, and consuming a custom CLI tool (git-semver) using the Debian packaging ecosystem and a private APT repository hosted on Nexus.

https://github.com/faustobranco/git-semver

Rather than focusing only on how, this guide emphasizes why each step exists, what problems it solves, and how all components fit together into a production-grade workflow.


1. Why Package a Binary Instead of Distributing It Directly?

At first glance, distributing a single Go binary via curl seems sufficient. However, packaging introduces several critical advantages:

  • Version management via APT
  • Dependency handling
  • Upgrade paths (apt upgrade)
  • Standardized installation locations
  • Traceability via dpkg
  • Enterprise compatibility

In short:

A .deb package turns a binary into a managed system artifact.


2. Cross-Compiling the Binary for Linux

The first step is ensuring the binary matches the target runtime environment.

GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=v0.2.1" -o git-semver

Why this matters

  • GOOS=linux: ensures compatibility with Linux systems (e.g., containers, servers)
  • GOARCH=amd64: matches common server architectures
  • -ldflags: injects version metadata at build time

Without this, the binary would not execute in a Linux environment.


3. Debian Package Structure: Understanding the Filesystem Layout

A Debian package is not just a compressed binary — it is a filesystem snapshot.

Directory Structure

git-semver-deb/
├── DEBIAN/
│   └── control
└── usr/
    └── local/
        └── bin/
            └── git-semver

Why this structure?

  • DEBIAN/: contains metadata and lifecycle scripts
  • usr/local/bin/: standard location for user-installed binaries

Debian packages mimic the root filesystem (/) — everything inside is installed relative to it.


4. Control File: The Package Identity

Package: git-semver
Version: 0.2.1
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Fausto <you@example.com>
Depends: git
Description: Simple semantic versioning tool for Git repositories

Why this matters

This file defines:

  • How APT identifies the package
  • Compatibility (Architecture)
  • Upgrade logic (Version)
  • Metadata for tooling (Section, Priority)

APT relies entirely on this metadata to resolve installations and upgrades.

Note on Dependencies: Depends: git

Since git-semver relies on Git to inspect repository history, it is important to declare this explicitly in the package metadata:

Depends: git

Why this matters

Debian packages should always declare their runtime dependencies. This ensures that:

  • Required tools are automatically installed
  • The package behaves predictably across systems
  • Failures are prevented at runtime

Without this declaration, the package may install successfully but fail when executed.

Practical impact

When installing:

apt install git-semver

APT will automatically resolve and install git if it is not already present.

This shifts responsibility from runtime checks to the package manager, which is the correct and expected behavior in Debian-based systems.


5. Lifecycle Scripts: Controlling Installation Behavior

Debian allows hooks at different lifecycle stages:

ScriptWhen it runs
preinstBefore installation
postinstAfter installation
prermBefore removal
postrmAfter removal

Example: postinst

#!/bin/bash
set -e

ln -sf /usr/local/bin/git-semver /usr/bin/git-semver

if ! command -v git >/dev/null; then
    echo "ERROR: git is required"
    exit 1
fi

echo "$(date) - git-semver installed"

chmod +x /usr/local/bin/git-semver

Why this matters

  • Ensures binary is available in /usr/bin
  • Validates runtime dependency (git)
  • Provides audit/logging

Lifecycle scripts turn a static package into a controlled installation process.

Folder Structure

In the end, the folder should have this structure before building the package.

.
└── git-semver-deb
    ├── DEBIAN
    │   ├── control
    │   └── postinst
    └── usr
        └── local
            └── bin
                └── git-semver

6. Building the Package

dpkg-deb --build --root-owner-group git-semver-deb

Why --root-owner-group?

Ensures files inside the package are owned by root, which is required for proper installation on target systems.


7. Inspecting the Package

dpkg-deb -c git-semver-deb.deb

drwxr-xr-x root/root         0 2026-03-26 16:24 ./
drwxr-xr-x root/root         0 2026-03-26 16:21 ./usr/
drwxr-xr-x root/root         0 2026-03-26 16:21 ./usr/local/
drwxr-xr-x root/root         0 2026-03-26 16:21 ./usr/local/bin/
-rwxr-xr-x root/root   3707525 2026-03-26 17:15 ./usr/local/bin/git-semver

Why inspect?

  • Verifies structure
  • Prevents incorrect paths
  • Ensures expected install locations

8. Local Installation Test

sudo dpkg -i git-semver-deb.deb

This bypasses APT and installs directly.

Why test this first?

  • Validates package integrity
  • Isolates packaging issues from repository issues

9. Clean Environment Testing with Docker

docker run -it --rm -v $(pwd):/work ubuntu:22.04 bash

Inside the container:

apt update
apt install -y ./work/git-semver-deb.deb
git-semver --version

Why use a container?

  • Guarantees clean environment
  • Avoids local machine contamination
  • Simulates real-world installation

11. Nexus APT Repository: Complete Setup and Configuration

Up to this point, we built a valid .deb package and validated it locally. However, the real power of the Debian ecosystem comes from centralized repositories.

This section explains how to configure a fully functional APT repository in Nexus, including why each configuration matters.


11.1 Why Use Nexus as an APT Repository?

Before diving into configuration, it’s important to understand the role Nexus plays:

  • Acts as a package distribution server
  • Generates and maintains APT metadata (Packages, Release files)
  • Provides versioned storage
  • Integrates with CI/CD pipelines
  • Supports access control and auditing

Nexus replaces the need for manual tools like reprepro or aptly.


11.2 Repository Type Selection

In Nexus Repository Manager, create a new repository:

Type: APT (hosted)

Why hosted?

  • You are publishing your own packages
  • Nexus becomes the source of truth
  • It generates metadata automatically

11.3 Core Configuration

Name

devops-db

This becomes part of your repository URL:

https://nexus.devops-db.internal/repository/devops-db/

Distribution

stable

Why stable Makes Sense Here

Your package (git-semver) is:

  • A statically compiled Go binary
  • Has no system-level dependencies
  • Works across multiple Ubuntu versions

Therefore:

Using stable decouples the package from OS versions.


Components

main

Why this exists

APT repositories are segmented:

  • main → primary packages
  • contrib, non-free → optional categories

For internal tools:

main is sufficient and standard


Architecture

amd64

Why this matters

Ensures:

  • Correct package filtering
  • Prevents incompatible installations

11.4 APT Signing Key (Critical for Production)

Step 1: Generate GPG Key

gpg --batch --generate-key gpg-batch.conf

Step 2: Export Public Key

gpg --armor --export "git-semver" > public.key

Step 3: Configure in Nexus

Upload public.key into:

APT Signing Key

Why signing is important

APT uses signatures to:

  • Verify package integrity
  • Prevent tampering
  • Ensure repository authenticity

Without signing, you must use:

[trusted=yes]

Which:

❌ disables security checks
✔ acceptable only for testing


11.5 Deployment Policy

Set:

Allow redeploy

Why?

During development:

  • You may rebuild the same version
  • You need flexibility

In production:

  • Prefer immutable versions

11.6 Blob Store

Choose:

default

Why?

Defines where artifacts are stored physically.

In larger setups:

  • Separate blob stores for isolation
  • Performance tuning


11.7 Final Repository URL Structure

After configuration, and upload the package, your repository behaves like:

Why this structure matters

APT expects:

  • dists/<distribution>/<component>/binary-<arch>/Packages
  • Release metadata
  • Optional InRelease signatures

Nexus generates all of this automatically.


11.8 Uploading Packages

curl -u usr_jenkins_nexus:1234qwer \
  -H "Content-Type: multipart/form-data" \
  --data-binary "@git-semver-deb.deb" \
  "https://nexus.devops-db.internal/repository/devops-db/"

Why this works

  • Nexus parses the .deb
  • Extracts metadata from control
  • Updates internal indexes
  • Regenerates Packages.gz

11.9 Repository Metadata Generation

After upload, Nexus automatically updates:

  • Packages.gz
  • Release
  • InRelease (if signing enabled)

Why this is critical

APT does not scan directories — it reads metadata.

If metadata is incorrect, packages are invisible.


11.10 Client Configuration

For testing pourpose, you can crate a Docker Container:

docker run -it --rm -v $(pwd):/work ubuntu:22.04 bash

Configuring Trust (Private CA)

In environments with internal certificates:

curl -O -k https://ca.devops-db.internal/roots.pem

mkdir -p /usr/local/share/ca-certificates/devops-db
cp roots.pem /usr/local/share/ca-certificates/devops-db/devops-db-root.crt

update-ca-certificates

Why this is necessary

  • APT requires HTTPS trust
  • Without this, repository access fails
  • Critical in enterprise environments
  • We have a Internal CA

Add repository

echo "deb [trusted=yes] https://nexus.devops-db.internal/repository/devops-db/ jammy main" > /etc/apt/sources.list.d/git-semver.list

Update APT

apt update

Install package

apt install git-semver

11.11 Verifying Repository Functionality

Check installation

git-semver --version

git-semver version v0.2.1

Check package metadata

dpkg -s git-semver | grep Version
Version: 0.2.1

Why both checks?

  • Binary version → confirms build correctness
  • Package version → confirms repository metadata

Leave a Reply

Your email address will not be published. Required fields are marked *