Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Build .deb, .rpm and .apk packages with fpm

By updated

Improve this page

fpm (Effing Package Management) builds .deb, .rpm and .apk packages from the same files, without becoming an expert in each distribution's native format. This guide shows how to install fpm, package a binary or a script as a Debian, RHEL and Alpine package, and add metadata, dependencies and installation scripts to it. It is written for administrators and developers who have already installed packages with apt or dnf. All outputs come from a real lab on Debian 13 (fpm 1.18.0, measured on 30 September 2026). The guide ends with a comparison with nfpm to choose the right tool.

What you will learn

  • Install fpm and the required native tools
  • Build a .deb, a .rpm and a .apk from the same sources
  • Add metadata, dependencies and installation scripts
  • Choose between fpm and nfpm

Prerequisites

  • A binary, a script or a directory to package
  • Ruby (fpm is written in Ruby) and the native tools of the target (dpkg for the .deb, rpm for the .rpm)

Why fpm

Each distribution has its own package format and tools: dpkg-deb and the debian/ files on the Debian side, rpmbuild and .spec files on the RHEL side. Learning each one takes time. fpm starts from a source (-s) and produces a target (-t), hiding that complexity:

  • Sources (-s): a directory (dir), a tar archive, a Ruby gem, a Python module, an npm package, or even an existing .deb/.rpm to convert.
  • Targets (-t): deb, rpm, apk (Alpine), pacman, tar, and others.

The same set of files therefore gives a package for all the targeted distributions, by changing a single argument.

Install fpm

fpm installs as a Ruby gem, after the system packages. Also install the native tool of each format you want to produce.

Terminal window
sudo apt update
sudo apt install -y ruby ruby-dev build-essential
# native tools of the wanted targets:
sudo apt install -y dpkg # to produce .deb
sudo apt install -y rpm # to produce .rpm
sudo gem install fpm

Check the installation:

Terminal window
fpm --version
1.18.0

Build a .deb from a directory

Prepare the tree to package, then run fpm. Here, a script installed in /usr/local/bin:

Terminal window
mkdir -p src/usr/local/bin
printf '#!/bin/sh\necho hello\n' > src/usr/local/bin/mytool
chmod 0755 src/usr/local/bin/mytool
fpm -s dir -t deb -n mytool -v 1.0.0 \
--description "Demo tool" --maintainer "Stephane" \
-C src usr/local/bin/mytool
{:timestamp=>"2026-09-30T15:45:54.446769+0000", :message=>"Created package", :path=>"mytool_1.0.0_amd64.deb"}

The -C src option (chdir) enters src before reading the files, so that the package tree starts at usr/local/bin. Check the result with the Debian tools:

Terminal window
dpkg -I mytool_1.0.0_amd64.deb # metadata
Package: mytool
Version: 1.0.0
Architecture: amd64
Maintainer: Stephane
Description: Demo tool
Terminal window
dpkg -c mytool_1.0.0_amd64.deb # contents
-rwxr-xr-x 0/0 23 2026-09-30 15:45 ./usr/local/bin/mytool
-rw-r--r-- 0/0 125 2026-09-30 15:45 ./usr/share/doc/mytool/changelog.gz

The real output also lists the parent directories (./usr/, ./usr/local/...), left out here for readability. The package can be installed locally with sudo apt install ./mytool_1.0.0_amd64.deb.

The same package as .rpm and .apk

To produce the other formats, only the -t target changes. The same file mapping gives a .rpm:

Terminal window
fpm -s dir -t rpm -n mytool -v 1.2.0 --iteration 1 \
--description "Demo tool" --maintainer "Stephane" --license MIT \
--depends bash \
src/usr/local/bin/mytool=/usr/local/bin/mytool

Here we use the source=destination syntax: the local file is placed at /usr/local/bin/mytool in the package. Check with the RPM tools:

Terminal window
rpm -qip mytool-1.2.0-1.x86_64.rpm
Name : mytool
Version : 1.2.0
Release : 1
License : MIT
Summary : Demo tool

And the Alpine format is generated the same way with -t apk:

Terminal window
fpm -s dir -t apk -n mytool -v 1.0 \
src/usr/local/bin/mytool=/usr/local/bin/mytool
{:timestamp=>"...", :message=>"apk output does not currently sign packages.", :level=>:warn}
{:timestamp=>"...", :message=>"It's recommended that your package be installed with '--allow-untrusted'", :level=>:warn}
{:timestamp=>"...", :message=>"Created package", :path=>"mytool_1.0_noarch.apk"}

Metadata, dependencies and scripts

Metadata is passed as options, identical whatever the target:

OptionRole
-n, -vpackage name and version
--iterationbuild number (the RPM release, the debian_revision)
-aarchitecture (amd64, noarch...)
--dependsa dependency (repeatable)
--description, -m, --url, --licensedescription, maintainer, URL, licence

fpm can also attach lifecycle scripts triggered at installation or removal: --before-install, --after-install, --before-remove, --after-remove.

Terminal window
printf '#!/bin/sh\necho post-install ran\n' > after.sh
fpm -s dir -t rpm -n mytool -v 1.2.0 --after-install after.sh \
src/usr/local/bin/mytool=/usr/local/bin/mytool

The script is indeed embedded, which you can check on the RPM side:

Terminal window
rpm -qp --scripts mytool-1.2.0-1.x86_64.rpm
postinstall scriptlet (using /bin/sh):
#!/bin/sh
echo post-install ran

fpm or nfpm: which one to choose

nfpm (from the GoReleaser team) is the modern alternative. The choice depends on your context.

Criterionfpmnfpm
Language / dependencyRuby (gem)standalone Go binary, zero dependencies
Configurationimperative (CLI)declarative (versioned nfpm.yaml)
Input sourcesdir, gem, python, npm, deb, rpm...files only (contents)
Targetsdeb, rpm, apk, pacman, tar...deb, rpm, srpm, apk, ipk, archlinux, msix
Package signing.rpm only, via --rpm-sign which delegates to rpmbuildnative (deb, rpm, apk)
CI/CD integrationto be scriptedbuilt into GoReleaser

Put simply: nfpm shines in a reproducible CI/CD pipeline (YAML config, single binary, native signing), especially to distribute a Go binary. fpm remains unbeatable when the input is not a simple directory: converting a gem, a Python module or repackaging a .rpm as a .deb, which nfpm does not do.

Pitfalls to know

fpm simplifies the build, but it does not decide for you what the package must contain or how it will be distributed. The four points below are the ones that make a package fail once installed, on the user's machine, even though the build went through without errors:

  • Ruby dependency: fpm requires Ruby and ruby-dev, where nfpm is a standalone binary. In CI, plan for installing the gem.
  • Partial signing: fpm signs neither .deb nor .apk files. For a .rpm, --rpm-sign passes --sign to rpmbuild, or you sign afterwards with rpm --addsign (rpm-sign package). On the Debian side, dpkg-sig no longer exists in Debian 13: debsigs signs a .deb, but APT does not check that signature, because trust relies on the repository signature.
  • Files in /etc: the Debian tools often mark everything under /etc as a configuration file, which changes the behaviour during updates. Check your paths.
  • No dependency resolution: --depends sets the metadata, but it is up to you to declare the right graph; fpm does not compute it.

Key points

  • fpm builds .deb, .rpm and .apk from the same sources: only -t changes.
  • It installs as a Ruby gem and requires the native tool of each target (dpkg, rpm).
  • -s dir + source=destination mapping is the most common mode; -C changes the base directory.
  • Metadata (-n, -v, --depends, --license) and scripts (--after-install...) are common to all targets.
  • fpm does not sign .deb or .apk packages, and signs a .rpm only through rpmbuild: sign after creation for public distribution.
  • For a reproducible Go pipeline, look at nfpm; to convert gem/python/npm, stay with fpm.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn