How to Package Scripts into Cross-Platform deb/rpm Installers with FPM
Stop manually copying files and configuring environments. Learn how to use FPM to package your scripts or applications into standard deb/rpm installers with a single command, enabling seamless cross-platform distribution for Linux systems.

Say Goodbye to Manual Copying: Package Your Scripts into deb/rpm with FPM
Have You Faced This Scenario?
Last week, I wrote a log cleanup script for the team. The functionality was fine, but distributing it was a nightmare:
- For colleagues on Ubuntu servers, I had to teach them how to place it in
/usr/local/bin, configure a systemd service, and set up environment variables. - For those on CentOS, I had to repeat the process, dealing with different paths and package manager commands.
- Then the test environment needed it, followed by staging...
Every deployment turned into a "hand-holding session," which was highly inefficient and prone to missed steps.
If you're still distributing internal tools or scripts via "file copying + manual configuration," this tutorial is for you. By the end, you'll be able to package any script or application into a standard deb/rpm installer with just one command. Recipients can simply run apt install ./your-tool.deb and have everything in place automatically: files deployed to the correct locations, permissions set, and dependencies declared.
1. Prerequisites
FPM is written in Ruby, so you only need Ruby and basic build tools in your environment. I verified this on Ubuntu 22.04 and CentOS 8, but it applies to other mainstream Linux distributions as well.
| Item | Minimum Requirement |
|---|---|
| Ruby | >= 2.6 |
| OS | Linux / macOS |
| Basic Tools | tar, gzip |
If your system doesn't have Ruby pre-installed (many servers don't), you can quickly install it via your package manager:
bash
## Ubuntu / Debian
sudo apt update
sudo apt install -y ruby ruby-dev gcc make
## CentOS / RHEL / Rocky
sudo dnf install -y ruby ruby-devel gcc make
Installing gcc and make is necessary because some Ruby gem dependencies for FPM require compiling native extensions. Without them, you'll hit mkmf.rb can't find header files for ruby. Installing them upfront saves troubleshooting time.
2. Install FPM
FPM is distributed via RubyGems, making installation straightforward:
bash
sudo gem install fpm
Verify the installation:
bash
$ fpm --version
1.15.1 # Version may vary; as long as it prints, installation succeeded
Why install via gem instead of apt/dnf? FPM updates frequently, and system repositories often lag behind. Using gem ensures you get a recent stable release, and its gem dependencies are well-managed with a low risk of conflicts.
3. Core Usage: Package with One Command
FPM's core philosophy is "tell me the source and target format, and I'll handle the rest." The basic syntax looks like this:
bash
fpm -s <source_type> -t <target_format> -n <package_name> -v <version> [other_options] <source_path>
Here are the most frequently used parameters (we'll apply them in the walkthrough):
| Parameter | Description |
|---|---|
-s |
Source type. dir (from a directory) is the most common. |
-t |
Target format. deb or rpm. |
-n |
Package name. |
-v |
Version number. |
-d |
Dependency declaration, e.g., -d "python3 >= 3.8". |
--prefix |
Installation path prefix. Defaults to / if omitted. |
-f |
Overwrites existing output files. |
4. Practical Walkthrough: Packaging a Python Script into deb and rpm
Scenario Setup
Assume you've written a Python script app-health-check.py to monitor service health. Requirements:
- Install to
/usr/local/bin/ - Generate a config file at
/etc/myapp/health.conf - Declare
python3as a dependency
Step 1: Prepare the Directory Structure
In your working directory, simulate the application layout:
bash
mkdir -p myapp/usr/local/bin
mkdir -p myapp/etc/myapp
echo '#!/usr/bin/env python3
print("Health check OK")' > myapp/usr/local/bin/health-check
chmod +x myapp/usr/local/bin/health-check
echo '{"endpoint": "localhost:8080", "interval": 60}' > myapp/etc/myapp/health.conf
The benefit of using a directory to mimic the filesystem layout is that the directory structure maps 1:1 to the final installed paths. Wherever you want a file to end up, place it in the corresponding directory. FPM will preserve this structure in the package.
Step 2: Build a deb Package (Debian/Ubuntu)
bash
fpm -s dir -t deb \
-n myapp-health \
-v 1.0.0 \
--description "Service health check tool" \
-m "Your Name <you@example.com>" \
-d "python3 >= 3.8" \
--url "https://github.com/you/myapp" \
--license "MIT" \
-f \
-C myapp \
usr/local/bin/health-check etc/myapp/health.conf
Key parameters explained:
-C myappchanges the working directory tomyappbefore packaging. This ensures the output package contains/usr/local/bin/health-checkinstead ofmyapp/usr/local/bin/health-check.-d "python3 >= 3.8"declares runtime dependencies. The installer will automatically check for them and block installation if they're missing.--licenseand--urlare embedded into the package metadata, aiding future audits and tracking.
After execution, you'll see myapp-health_1.0.0_amd64.deb in the current directory.
Step 3: Build an rpm Package (CentOS/RHEL/Rocky)
bash
fpm -s dir -t rpm \
-n myapp-health \
-v 1.0.0 \
--description "Service health check tool" \
-m "Your Name <you@example.com>" \
-d "python3 >= 3.8" \
--license "MIT" \
-f \
-C myapp \
usr/local/bin/health-check etc/myapp/health.conf
The parameters are nearly identical; we simply swapped -t deb for -t rpm. This is FPM's greatest value: the exact same source files output different platform packages by changing just one flag. You'll get myapp-health-1.0.0-1.x86_64.rpm.
Step 4: Installation & Verification
bash
## Debian/Ubuntu
sudo dpkg -i myapp-health_1.0.0_amd64.deb
## CentOS/RHEL
sudo rpm -ivh myapp-health-1.0.0-1.x86_64.rpm
## Or use yum/dnf (recommended for automatic dependency resolution)
sudo dnf install -y ./myapp-health-1.0.0-1.x86_64.rpm
Test it out:
bash
$ health-check
Health check OK
$ ls /etc/myapp/health.conf
/etc/myapp/health.conf
Files are in place, permissions are correct, and execution works out of the box. That's exactly what a standard package should do.
5. Common Pitfalls & Troubleshooting
1. rpmbuild: command not found when installing an RPM
This means your system lacks the rpm-build tool. FPM invokes rpmbuild internally to construct RPMs. Fix it with:
bash
## CentOS/RHEL/Rocky
sudo dnf install -y rpm-build
## Ubuntu
sudo apt install -y rpm
2. Extra Directory Layer After Installation
If you forget the -C <source_dir> flag or write paths incorrectly, files might install to /myapp/usr/local/bin/ instead of /usr/local/bin/. Remember: -C changes the build context directory, not the output directory.
3. Missing Executable Permissions on Scripts
FPM preserves original file permissions. However, if you didn't mark files as +x in git, or created them on Windows before transferring to Linux, permissions might drop. Run chmod +x before packaging, or use --before-install to run a chmod script during installation.
4. Auto-Start Services on Installation?
Use the --after-install flag to specify a post-installation script:
bash
fpm ... --after-install ./post-install.sh
Inside post-install.sh, you can add systemctl enable myapp && systemctl start myapp.
6. Conclusion
Let's recap today's workflow:
- Prepare the directory — Organize files according to their final installation paths.
- Run one
fpmcommand — Specify source type, target format, package name, version, and dependencies. - Swap the
-tflag — Outputdeborrpmfrom the same source files. - Verify installation — Deploy with
dpkg -ioryum installin one step.
For advanced usage (packaging from tarballs, modifying existing package metadata, stripping install scripts, etc.), check out the official docs at fpm.readthedocs.io.
Next Steps: Try packaging a small utility you've recently written. Even a few scripts plus a config file, once packaged as deb/rpm and shared with colleagues, will noticeably improve your distribution efficiency and deployment reliability. Once comfortable, dive into --before-install/--after-install hooks and --deb-systemd for automated systemd service generation.
Got packaging questions? Drop a comment below—I usually reply in the comments section.