I need to learn more about RPM packages, and I figured a good way to do that would be packaging my little Patissiere application. Before I do that though, I need a systemd unit file.
So a common complaint I’ve heard from systemd detractors is that the old school init scripts were really easy to read / write and that this newfangled systemd stuff is a lot more difficult.
patissiere.service
[Unit]
Description=Patissiere pastebin service
After=network.target
[Service]
Type=simple
User=patissiere
ExecStart=/usr/local/bin/patissiere
WorkingDirectory=/var/lib/patissiere
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Honestly, I was expecting it to be more than this! We just give it a description, tell systemd when to turn on Patissiere, (after network.target, since we need network) and then tell systemd what user to use and where to run everything.
multi-user.target means start the service when reaching multi-user mode. It’s convention to start network services here.
Straightforward! I think this is all I need in advance to make a spec file. Let’s start with the header section:
*patissiere.spec
Name: patissiere
Version: 0.1.1
Release: 1%{?dist}
Summary: A simple pastebin written in Rust
License: AGPL-3.0
URL: https://git.toasterdragon.com/butter/patissiere
BuildArch: x86_64
Name, Version, and Summary should be pretty obvious. In Release, %{?dist} adds a distro tag, on RHEL 10 or Rocky 10 (what I’m using) it should expand to something like .el10
License, I went with AGPL for this project. I know I’m definitely not at risk of some corporate entity using this dinky little project, but it just felt like the right choice.
BuildArch is easy to set since I’m providing a precompiled binary. I will have to change this when I provide source instead of a binary. But that seems to increase the complexity a lot, so I’m going to start with just a binary. I’ve compiled one with cargo build --release. Anyways, there’s more to this spec file:
%description
Patissiere is a lightweight self-hosted pastebin service written in Rust using Axum.
longer description, shows up in dnf info patissiere. This is sort of for flavour but is expected.
%prep
# using prebuilt binary
%install
mkdir -p %{buildroot}/usr/local/bin
mkdir -p %{buildroot}/usr/lib/systemd/system
mkdir -p %{buildroot}/var/lib/patissiere
install -m 755 %{_sourcedir}/patissiere %{buildroot}/usr/local/bin/patissiere
install -m 644 %{_sourcedir}/patissiere.service %{buildroot}/usr/lib/systemd/system/patissiere.service
Okay, we can ignore the %prep, since we aren’t using the build service. I’m told that some versions of rpmbuild expect it there, so I have to have it.
Next, %install lets us put files into %{buildroot} which is a fake root env that rpmbuild uses to figure out where to put files, kind of like a staging area. %{_sourcedir} is an RPM macro that expands in my case to ~/rpmbuild/SOURCES/. install -m 755 copies the file and sets permissions in one command, very useful.
%files
/usr/local/bin/patissiere
/usr/lib/systemd/system/patissiere.service
%dir /var/lib/patissiere
%files defines what files the package owns. If a file isn’t listed here, it won’t be included in the rpm.
%pre
getent passwd patissiere > /dev/null || \
useradd --system --no-create-home --home-dir /var/lib/patissiere patissiere
%post
systemctl daemon-reload
Here we define what runs before and after installation. Before we create the system user if it doesn’t exist already, and after we reload systemd so it knows about the service.
%preun
systemctl stop patissiere || true
systemctl disable patissiere || true
%postun
systemctl daemon-reload
Same as before, but for uninstallation. Before we stop and disable the service, then after we reload systemd again so it knows the service is gone.
%changelog
* Sun Mar 29 2026 Butter <butter@toasterdragon.com> - 0.1.1-1
- Initial package
Easy enough, just a changelog! I’ll update this with patch summaries as I increment version.
That’s it!
Next, I need to make the actual rpm. My laptop runs arch, but rpm-tools is in the arch repos! I’ll go ahead and install it now.
:: Processing package changes...
(1/3) installing elfutils
(2/3) installing rpm-sequoia
(3/3) installing rpm-tools
rpm-tools installs RedHat package manager for you.
It is useful for those who wants to create/modify RPM files.
But do not use rpm-tools to install RedHat packages at your ArchLinux machine.
It will break your system!
You will need to go back to Arch wiki and read the installation guide again.
You've been warned!
:: Running post-transaction hooks...
(1/2) Arming ConditionNeedsUpdate...
(2/2) Performing snapper post snapshots for the following configurations...
hehehe. It seems like people have gotten themselves in trouble trying to use this to install rpm packages on arch. Silly.
Anyways, rpmbuild expects everything in ~, so I’ve went ahead and created the directory structure with:
butter@stinkbook ~
❯ mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
Easy enough. Now copy the files where they need to go:
cp pkg/patissiere.spec ~/rpmbuild/SPECS/
cp target/release/patissiere ~/rpmbuild/SOURCES/
cp pkg/patissiere.service ~/rpmbuild/SOURCES/
Then, all the files are where they need to go, I can try to build the rpm:
rpmbuild -bb ~/rpmbuild/SPECS/patissiere.spec
Among a bunch of other output, that gave me:
Wrote: /home/butter/rpmbuild/RPMS/x86_64/patissiere-0.1.1-1.x86_64.rpm
Easy! Now to copy it over to the server…
scp ~/rpmbuild/RPMS/x86_64/patissiere-0.1.1-1.x86_64.rpm butter@toasterdragon.com:~
And now on the server, time for the real shit certified moment, trying to install the package:
butter@butter-rocky ~> sudo rpm -ivh patissiere-0.1.1-1.x86_64.rpm
[sudo] password for butter:
Verifying... ################################# [100%]
Preparing... ################################# [100%]
Updating / installing...
1:patissiere-0.1.1-1 ################################# [100%]
butter@butter-rocky ~> sudo systemctl status patissiere
○ patissiere.service - Patissiere pastebin service
Loaded: loaded (/usr/lib/systemd/system/patissiere.service; disabled; preset: disabled)
Active: inactive (dead)
butter@butter-rocky ~ [3]>
SHRIMPLE AS!
Okay, enabling the service and starting it.
butter@butter-rocky ~> sudo rpm -ivh patissiere-0.1.1-1.x86_64.rpm
[sudo] password for butter:
Verifying... ################################# [100%]
Preparing... ################################# [100%]
Updating / installing...
1:patissiere-0.1.1-1 ################################# [100%]
butter@butter-rocky ~> sudo systemctl status patissiere
○ patissiere.service - Patissiere pastebin service
Loaded: loaded (/usr/lib/systemd/system/patissiere.service; disabled; preset: disabled)
Active: inactive (dead)
butter@butter-rocky ~ [3]> sudo systemctl enable --now patissiere
Created symlink '/etc/systemd/system/multi-user.target.wants/patissiere.service' → '/usr/lib/systemd/system/patissiere.service'.
butter@butter-rocky ~> sudo systemctl status patissiere
● patissiere.service - Patissiere pastebin service
Loaded: loaded (/usr/lib/systemd/system/patissiere.service; enabled; preset: disabled)
Active: activating (auto-restart) (Result: exit-code) since Mon 2026-03-30 20:02:51 PDT; 3s ago
Invocation: f38734875c2e4cf795104a45d9c16e89
Process: 58299 ExecStart=/usr/local/bin/patissiere (code=exited, status=101)
Main PID: 58299 (code=exited, status=101)
Mem peak: 1.2M
CPU: 6ms
Mar 30 20:02:51 butter-rocky systemd[1]: patissiere.service: Main process exited, code=exited, status=101/n/a
Mar 30 20:02:51 butter-rocky systemd[1]: patissiere.service: Failed with result 'exit-code'.
butter@butter-rocky ~ [3]>
Okay, seems like we’re crashing at startup. Let’s investigate.
butter@butter-rocky ~ [3]> sudo journalctl -u patissiere -n 20
Mar 30 20:04:20 butter-rocky systemd[1]: Started patissiere.service - Patissiere pastebin service.
Mar 30 20:04:20 butter-rocky patissiere[58409]: thread 'main' (58409) panicked at src/main.rs:21:42:
Mar 30 20:04:20 butter-rocky patissiere[58409]: called `Result::unwrap()` on an `Err` value: Os { code: 13, ki>
Mar 30 20:04:20 butter-rocky patissiere[58409]: note: run with `RUST_BACKTRACE=1` environment variable to disp>
Mar 30 20:04:20 butter-rocky systemd[1]: patissiere.service: Main process exited, code=exited, status=101/n/a
Mar 30 20:04:20 butter-rocky systemd[1]: patissiere.service: Failed with result 'exit-code'.
Mar 30 20:04:26 butter-rocky systemd[1]: patissiere.service: Scheduled restart job, restart counter is at 18.
Mar 30 20:04:26 butter-rocky systemd[1]: Started patissiere.service - Patissiere pastebin service.
Mar 30 20:04:26 butter-rocky patissiere[58414]: thread 'main' (58414) panicked at src/main.rs:21:42:
Mar 30 20:04:26 butter-rocky patissiere[58414]: called `Result::unwrap()` on an `Err` value: Os { code: 13, ki>
Mar 30 20:04:26 butter-rocky patissiere[58414]: note: run with `RUST_BACKTRACE=1` environment variable to disp>
Mar 30 20:04:26 butter-rocky systemd[1]: patissiere.service: Main process exited, code=exited, status=101/n/a
Mar 30 20:04:26 butter-rocky systemd[1]: patissiere.service: Failed with result 'exit-code'.
Mar 30 20:04:31 butter-rocky systemd[1]: patissiere.service: Scheduled restart job, restart counter is at 19.
Mar 30 20:04:31 butter-rocky systemd[1]: Started patissiere.service - Patissiere pastebin service.
Mar 30 20:04:31 butter-rocky patissiere[58422]: thread 'main' (58422) panicked at src/main.rs:21:42:
Mar 30 20:04:31 butter-rocky patissiere[58422]: called `Result::unwrap()` on an `Err` value: Os { code: 13, ki>
Mar 30 20:04:31 butter-rocky patissiere[58422]: note: run with `RUST_BACKTRACE=1` environment variable to disp>
Mar 30 20:04:31 butter-rocky systemd[1]: patissiere.service: Main process exited, code=exited, status=101/n/a
Mar 30 20:04:31 butter-rocky systemd[1]: patissiere.service: Failed with result 'exit-code'.
lines 1-20/20 (END)
Okay, that’s simple, Error code 13 is permission denied. I don’t think patissiere owns the folder we set up for it, /var/lib/patissiere. Fixing perms:
sudo chown patissiere:patissiere /var/lib/patissiere
sudo systemctl restart patissiere
Hm, still failing. Do we even have the user?
butter@butter-rocky ~> getent passwd patissiere
patissiere:x:991:991::/var/lib/patissiere:/bin/bash
Yeah, we do… Hm.
Checking the logs, I see:
Mar 30 20:10:12 butter-rocky patissiere[59149]: called `Result::unwrap()` on an `Err` value: Os { code: 98,
Code 98 is Address already in use… hmm… something is using port 3000 already?
butter@butter-rocky ~> sudo ss -tlnp | grep 3000
LISTEN 0 4096 *:3000 *:* users:(("gitea",pid=905,fd=13))
Oh. Boring. It’s gitea. I’ll have to repackage the rpm to update the port. Back on my laptop, I’ve edited patissiere.service to start with port 3005 (hold up!). Incremented release to 2, and added notes for why. After that, I’ve just gotta copy it over and redo the rpm build, then copy it over to the server.
Over there, I can do a simple
butter@butter-rocky ~> sudo rpm -Uvh patissiere-0.1.1-2.x86_64.rpm
The -U means Update! Anyways, after enabling the service, time to check:
butter@butter-rocky ~> sudo systemctl status patissiere
● patissiere.service - Patissiere pastebin service
Loaded: loaded (/usr/lib/systemd/system/patissiere.service; enabled; preset: disabled)
Active: active (running) since Mon 2026-03-30 20:25:38 PDT; 5s ago
Invocation: e5e06427900f40369696a3fd6af04e58
Main PID: 60587 (patissiere)
Tasks: 4 (limit: 23258)
Memory: 480K (peak: 1.2M)
CPU: 5ms
CGroup: /system.slice/patissiere.service
└─60587 /usr/local/bin/patissiere --port 3005
Mar 30 20:25:38 butter-rocky systemd[1]: Started patissiere.service - Patissiere pastebin service.
Mar 30 20:25:39 butter-rocky patissiere[60587]: patissiere listening on port 3005
CHIRRRR~ It seems like we’re working!
Quickly configure nginx:
/etc/nginx/conf.d/pastry.toasterdragon.com.conf
server {
server_name pastry.toasterdragon.com;
location / {
proxy_pass http://localhost:3005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Shrimple as. After running certbot, it seems like my site is UP!
Check it out at pastry.toasterdragon.com!