RPM Packaging on CentOS Stream 9: Stop SSH-ing and Copy-Pasting Code!

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

Don’t Let Deployment Be Your Nightly Nightmare

Have you ever stayed up until 2 AM just to fix a “silly” error caused by a missing config file on one out of 30 servers? Early in my career, I often found myself with exhausted hands from typing SSH commands, pulling code from Git, and then painstakingly running chmod on every single node. When a system scales from 5 to 50 servers, this manual method becomes a ticking time bomb. One small forgotten step, and the entire cluster can go down.

Packaging applications into RPM (Red Hat Package Manager) files is how professional DevOps engineers solve this problem. Instead of sending a messy pile of files, you only need to transfer a single .rpm file. The system will automatically check dependencies and place files in their correct locations. Best of all is the ability to perform a clean rollback using the dnf remove command if something goes wrong.

After six months of applying this workflow to a banking system on CentOS Stream 9, I’ve distilled the most concise roadmap. You will master rpmbuild without having to read through mountains of dry Red Hat documentation.

Hands-on: “Gearing Up” for Your First RPM

Before starting, prepare a clean build environment. One vital note: absolutely do not use the root user. A small mistake in a build script could wipe out your system data in an instant.

# Install the standard toolset
sudo dnf install -y rpm-build rpmdevtools gcc

# Initialize the build directory structure
rpmdev-setuptree

The command above will create a ~/rpmbuild directory. Inside, you should focus on SOURCES (containing the original code) and SPECS (containing the build configuration files). Let’s try packaging a simple script named hello-itfromzero.sh to understand the workflow.

# Create a sample source file
echo 'echo "Welcome to itfromzero.com!"' > ~/rpmbuild/SOURCES/hello-itfromzero.sh

Next, we create the SPEC file—this is the blueprint for your package:

nano ~/rpmbuild/SPECS/hello.spec

Paste the following content inside:

Name:           hello-itfromzero
Version:        1.0
Release:        1%{?dist}
Summary:        Welcome script from itfromzero
License:        GPL
Source0:        hello-itfromzero.sh
BuildArch:      noarch

%description
A simple script to demo RPM building on CentOS Stream 9.

%install
mkdir -p %{buildroot}/usr/bin/
cp %{_sourcedir}/hello-itfromzero.sh %{buildroot}/usr/bin/hello-itfromzero
chmod +x %{buildroot}/usr/bin/hello-itfromzero

%files
/usr/bin/hello-itfromzero

Finally, trigger the build command:

rpmbuild -ba ~/rpmbuild/SPECS/hello.spec

In just about 3-5 seconds, your RPM file will appear in the ~/rpmbuild/RPMS/noarch/ directory. Now, you can take this file and install it on any other CentOS 9 machine with a single dnf install command.

Dissecting the SPEC File: Components You Must Remember

In practice, SPEC files are much more complex than the example above. Here are three key points I frequently deal with when packaging Java or Go applications.

1. Dependency Management with Requires

This is where you define prerequisites. For example: Requires: python3, nginx >= 1.20. When a user installs the package, dnf will automatically calculate and pull in any missing libraries. This is extremely effective at preventing “library not found” errors.

2. %install – The Sandbox

Many beginners mistake %install for a command that runs when the RPM is installed on the client machine. In reality, it runs during the build process. It copies files from the code directory into a temporary folder called BuildRoot. Think of BuildRoot as a virtual operating system. However you arrange files there, the RPM will place them identically on the actual machine during installation.

3. %post and %pre: Post-Installation Automation

These are scripts that run directly on the target machine. You can use %pre to create system users before installation, or use %post to start a service immediately after installation finishes.

Advanced: Packaging Applications that Run on Systemd

In an enterprise environment, almost every app must run as a service. On CentOS Stream 9, you should leverage systemd macros to ensure stability. Don’t manually type systemctl reload; let the RPM handle it.

# Inside the %install section
install -D -m 644 %{SOURCE1} %{buildroot}%{_unitdir}/my-app.service

# Automatically register the service after installation
%post
%systemd_post my-app.service

# Clean up the service upon removal
%preun
%systemd_preun my-app.service

Using macros like %systemd_post helps the system safely reload the daemon. This approach is far more professional than writing fragmented bash scripts.

Real-world Experience: Pitfalls I’ve Encountered

When migrating systems from CentOS 7 to CentOS Stream 9, I ran into quite a few headaches. Here are some hard-learned lessons to help you avoid the same mistakes:

  • “Installed but unpackaged” error: If you copy a file into BuildRoot but forget to list it in the %files section, the build process will fail immediately. Every file must be fully accounted for.
  • Configuration file conflicts: Never overwrite system files directly. Create a dedicated directory like /etc/my-company/app.conf to avoid conflicts with other packages.
  • Use Mock for building: To ensure the RPM works on every machine (avoiding the “it works on my machine” syndrome), I recommend using mock. This tool creates a 100% clean chroot environment for building, helping you catch missing dependencies early on.

RPM packaging might seem like more effort initially compared to a simple bash script. However, when you need to manage versions across hundreds of servers, RPM is the insurance policy that helps you sleep better at night. Good luck mastering this packaging process!

Share: