Initial semaphore 2.19.8 package with service, container and postgresql subpackages

Semaphore UI packaged from the upstream community (MIT) release
binaries, following the gitea packaging pattern: native systemd service
and Podman quadlet variants (mutually exclusive), with an optional
PostgreSQL backend via quadlet drop-ins. SQLite is the default
database; session/encryption secrets are generated into
/etc/semaphore/env on first install.
This commit is contained in:
2026-08-18 12:22:56 +02:00
commit 2e971845d8
17 changed files with 408 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Downloaded upstream release tarballs (spectool -g semaphore.spec)
semaphore_community_*_linux_amd64.tar.gz
semaphore_community_*_linux_arm64.tar.gz
+70
View File
@@ -0,0 +1,70 @@
# semaphore
Semaphore UI (https://semaphoreui.com) packaged for InfoMaOS. Web UI and
API for running Ansible, Terraform, OpenTofu, Terragrunt, Bash and
PowerShell tasks.
Binaries are the upstream **community** (fully open source, MIT) release
builds. Download the source tarballs with:
spectool -g semaphore.spec
## Subpackages
- `semaphore` — common base: `semaphore` user/group (uid/gid 127),
`/etc/semaphore/config.json`, `/etc/semaphore/env` (secrets generated
on first install), `/var/lib/semaphore` data directory.
- `semaphore-service` — native binary + `semaphore.service` unit.
Requires `git` and `ansible`. Conflicts with `semaphore-container`.
- `semaphore-container` — Podman quadlet running
`docker.io/semaphoreui/semaphore:v<version>` in a pod
(`semaphore-pod`, web UI published on port 3000). The image bundles
Ansible, Terraform, OpenTofu and Terragrunt. Conflicts with
`semaphore-service`.
- `semaphore-postgresql` — optional PostgreSQL backend as a
`semaphore-db` quadlet container. With `-service` it runs standalone,
published on `127.0.0.1:5432`; with `-container` it joins the
semaphore pod. Without it, Semaphore uses SQLite at
`/var/lib/semaphore/database.sqlite` (zero configuration).
## Database
Default dialect is SQLite (`config.json`). Installing
`semaphore-postgresql` switches the dialect to PostgreSQL via
`SEMAPHORE_DB_*` environment drop-ins:
- service mode: `/etc/systemd/system/semaphore.service.d/database.conf`
- container mode: `/usr/share/containers/systemd/semaphore.container.d/database.conf`
Change the default `semaphore`/`semaphore` credentials in **both** the
relevant `database.conf` and
`/etc/containers/systemd/semaphore-db.container.d/credentials.conf`
before first start.
## First start
```bash
systemctl enable --now semaphore.service # service mode
systemctl start semaphore.service # container mode (enabled via pod drop-in)
# Create the initial admin user
semaphore user add --admin --login admin --name Admin \
--email admin@example.org --password '<password>' \
--config /etc/semaphore/config.json
```
In container mode `/usr/bin/semaphore` is a shim that runs the CLI
inside the running container.
Session/encryption secrets (`SEMAPHORE_COOKIE_HASH`,
`SEMAPHORE_COOKIE_ENCRYPTION`, `SEMAPHORE_ACCESS_KEY_ENCRYPTION`) are
generated into `/etc/semaphore/env` by the base package `%post` on first
install. Back up this file — `SEMAPHORE_ACCESS_KEY_ENCRYPTION` encrypts
the stored SSH keys and secrets, which are lost if it changes.
## Updating
1. Bump `Version` in the spec and the image tag in
`semaphore.container`.
2. `spectool -g semaphore.spec`
3. `./build.sh`
+8
View File
@@ -0,0 +1,8 @@
{
"dialect": "sqlite",
"sqlite": {
"host": "/var/lib/semaphore/database.sqlite"
},
"port": ":3000",
"tmp_path": "/var/lib/semaphore/tmp"
}
+14
View File
@@ -0,0 +1,14 @@
# Switches the semaphore container from the default SQLite database to
# the semaphore-db PostgreSQL container running in the same pod.
# Credentials must match
# /etc/containers/systemd/semaphore-db.container.d/credentials.conf.
[Unit]
Wants=semaphore-db.service
After=semaphore-db.service
[Container]
Environment=SEMAPHORE_DB_DIALECT=postgres
Environment=SEMAPHORE_DB_HOST=127.0.0.1:5432
Environment=SEMAPHORE_DB_USER=semaphore
Environment=SEMAPHORE_DB_PASS=semaphore
Environment=SEMAPHORE_DB=semaphore
+13
View File
@@ -0,0 +1,13 @@
# Switches semaphore.service (native binary) from the default SQLite
# database to the semaphore-db PostgreSQL container. Credentials must
# match /etc/containers/systemd/semaphore-db.container.d/credentials.conf.
[Unit]
Wants=semaphore-db.service
After=semaphore-db.service
[Service]
Environment=SEMAPHORE_DB_DIALECT=postgres
Environment=SEMAPHORE_DB_HOST=127.0.0.1:5432
Environment=SEMAPHORE_DB_USER=semaphore
Environment=SEMAPHORE_DB_PASS=semaphore
Environment=SEMAPHORE_DB=semaphore
+4
View File
@@ -0,0 +1,4 @@
[Container]
Environment=POSTGRES_USER=semaphore
Environment=POSTGRES_PASSWORD=semaphore
Environment=POSTGRES_DB=semaphore
+2
View File
@@ -0,0 +1,2 @@
[Container]
Pod=semaphore.pod
+6
View File
@@ -0,0 +1,6 @@
# Used with semaphore-service: the database container runs standalone
# (outside the pod), so it must publish its port on the loopback and run
# directly as the host semaphore user to own the data directory.
[Container]
User=127:127
PublishPort=127.0.0.1:5432:5432
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=Semaphore UI PostgreSQL Database
[Container]
Image=docker.io/library/postgres:18.1-trixie
ContainerName=semaphore-db
User=1001:1001
Volume=/var/lib/semaphore/postgresql:/var/lib/postgresql:z
Exec=postgres -c shared_buffers=256MB -c min_wal_size=80MB
HealthCmd=pg_isready -U semaphore
HealthInterval=30s
HealthTimeout=10s
HealthRetries=5
HealthStartPeriod=1m
HealthOnFailure=kill
LogDriver=journald
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=900
+2
View File
@@ -0,0 +1,2 @@
[Install]
WantedBy=multi-user.target
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec podman exec -i semaphore semaphore "$@"
+25
View File
@@ -0,0 +1,25 @@
[Unit]
Description=Semaphore UI - modern UI for Ansible, Terraform and OpenTofu
[Container]
Image=docker.io/semaphoreui/semaphore:v2.19.8
ContainerName=semaphore
Pod=semaphore.pod
Volume=/var/lib/semaphore:/var/lib/semaphore:z
Volume=/etc/semaphore:/etc/semaphore:z
Volume=/etc/localtime:/etc/localtime:ro
EnvironmentFile=/etc/semaphore/env
Environment=SEMAPHORE_CONFIG_PATH=/etc/semaphore
Notify=healthy
HealthCmd=curl --fail --silent --output /dev/null http://localhost:3000/api/ping
HealthInterval=30s
HealthTimeout=10s
HealthRetries=5
HealthStartPeriod=120s
HealthOnFailure=kill
LogDriver=journald
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=900
+6
View File
@@ -0,0 +1,6 @@
# Environment for Semaphore UI (read by semaphore.service and the
# semaphore quadlet container). Secrets are generated on first install.
SEMAPHORE_CONFIG=/etc/semaphore/config.json
SEMAPHORE_COOKIE_HASH=
SEMAPHORE_COOKIE_ENCRYPTION=
SEMAPHORE_ACCESS_KEY_ENCRYPTION=
+5
View File
@@ -0,0 +1,5 @@
[Pod]
PodName=semaphore-pod
PublishPort=3000:3000
UIDMap=0:0:127 127:1001:1 128:128:873 1001:127:1 1002:1002:64535
GIDMap=0:0:127 127:1001:1 128:128:873 1001:127:1 1002:1002:64535
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Semaphore UI - modern UI for Ansible, Terraform and OpenTofu
Documentation=https://docs.semaphoreui.com/
Wants=network-online.target
After=network-online.target
[Service]
User=semaphore
Group=semaphore
WorkingDirectory=/var/lib/semaphore
EnvironmentFile=/etc/semaphore/env
ExecStart=/usr/libexec/semaphore server --config /etc/semaphore/config.json
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
+207
View File
@@ -0,0 +1,207 @@
Name: semaphore
Version: 2.19.8
Release: 1%{?dist}
Summary: Semaphore UI - modern UI for Ansible, Terraform, OpenTofu and other DevOps tools
License: MIT
Group: System Environment/Base
URL: https://semaphoreui.com
# Upstream community (fully open source) release binaries, download with
# spectool -g semaphore.spec
Source0: https://github.com/semaphoreui/semaphore/releases/download/v%{version}/semaphore_community_%{version}_linux_amd64.tar.gz
Source1: https://github.com/semaphoreui/semaphore/releases/download/v%{version}/semaphore_community_%{version}_linux_arm64.tar.gz
Source10: semaphore.service
Source11: semaphore.sysusers
Source12: config.json
Source13: semaphore.env
Source14: semaphore-shim
Source15: semaphore.container
Source16: semaphore.pod
Source17: semaphore-pod-enable.conf
Source18: semaphore-database.conf
Source19: semaphore-container-database.conf
Source20: semaphore-db.container
Source21: semaphore-db-pod.conf
Source22: semaphore-db-publish.conf
Source23: semaphore-db-credentials.conf
ExclusiveArch: x86_64 aarch64
BuildRequires: systemd-rpm-macros
%description
Semaphore UI is a modern web interface and API for running Ansible
playbooks, Terraform, OpenTofu, Terragrunt, Bash and PowerShell tasks
with notifications, access control and audit trail.
%package service
Summary: Semaphore UI systemd service (native binary)
Requires: %{name} = %{version}-%{release}
Requires: git
Requires: ansible
Conflicts: %{name}-container
%{?systemd_requires}
%description service
Runs Semaphore UI as a native binary under systemd. Includes the
semaphore binary and systemd unit. Uses the SQLite database by default.
%package container
Summary: Semaphore UI container (Podman quadlet)
Requires: %{name} = %{version}-%{release}
Requires: podman
Requires: containers-common
Conflicts: %{name}-service
%description container
Runs Semaphore UI as a Podman container via quadlet. The upstream image
bundles Ansible, Terraform, OpenTofu and Terragrunt.
%package postgresql
Summary: PostgreSQL database support for Semaphore UI
Requires: podman
Requires: containers-common
Requires: %{name} = %{version}-%{release}
%description postgresql
Provides PostgreSQL database integration for Semaphore UI. When used
with semaphore-service, runs a standalone PostgreSQL container
(semaphore-db) published on the loopback. When used with
semaphore-container, runs the PostgreSQL container in the semaphore pod.
%install
%{__rm} -rf %{buildroot}
# Binary (for -service subpackage) → libexec
install -d -m 755 %{buildroot}%{_libexecdir}
%ifarch x86_64
tar -xzf %{SOURCE0} -C . semaphore
%endif
%ifarch aarch64
tar -xzf %{SOURCE1} -C . semaphore
%endif
install -m 755 semaphore %{buildroot}%{_libexecdir}/semaphore
# Shim (for -container subpackage)
install -m 755 %{SOURCE14} %{buildroot}%{_libexecdir}/semaphore-container
# Base: sysusers, config, env
install -p -D -m 644 %{SOURCE11} %{buildroot}%{_sysusersdir}/semaphore.conf
install -d -m 750 %{buildroot}%{_sysconfdir}/semaphore
install -m 640 %{SOURCE12} %{buildroot}%{_sysconfdir}/semaphore/config.json
install -m 600 %{SOURCE13} %{buildroot}%{_sysconfdir}/semaphore/env
# Base: data dirs
install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore
install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore/tmp
# Service subpackage
install -p -D -m 644 %{SOURCE10} %{buildroot}%{_unitdir}/semaphore.service
install -p -D -m 644 %{SOURCE22} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container.d/publish.conf
# Container subpackage
install -p -D -m 644 %{SOURCE15} %{buildroot}%{_datadir}/containers/systemd/semaphore.container
install -p -D -m 644 %{SOURCE16} %{buildroot}%{_datadir}/containers/systemd/semaphore.pod
install -p -D -m 644 %{SOURCE17} %{buildroot}%{_sysconfdir}/containers/systemd/semaphore.pod.d/enable.conf
# PostgreSQL subpackage
install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore/postgresql
install -p -D -m 644 %{SOURCE18} %{buildroot}%{_sysconfdir}/systemd/system/semaphore.service.d/database.conf
install -p -D -m 644 %{SOURCE19} %{buildroot}%{_datadir}/containers/systemd/semaphore.container.d/database.conf
install -p -D -m 644 %{SOURCE20} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container
install -p -D -m 644 %{SOURCE21} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container.d/pod.conf
install -p -D -m 644 %{SOURCE23} %{buildroot}%{_sysconfdir}/containers/systemd/semaphore-db.container.d/credentials.conf
%pre
%sysusers_create_compat %{SOURCE11}
%post
# Generate session/encryption secrets on first install (empty values only)
for key in SEMAPHORE_COOKIE_HASH SEMAPHORE_COOKIE_ENCRYPTION SEMAPHORE_ACCESS_KEY_ENCRYPTION; do
if grep -q "^${key}=$" %{_sysconfdir}/semaphore/env 2>/dev/null; then
secret=$(head -c 32 /dev/urandom | base64 -w0)
sed -i "s|^${key}=$|${key}=${secret}|" %{_sysconfdir}/semaphore/env
fi
done
%post service
%systemd_post semaphore.service
%posttrans service
ln -sf %{_libexecdir}/semaphore %{_bindir}/semaphore
%preun service
%systemd_preun semaphore.service
%postun service
%systemd_postun semaphore.service
if [ $1 -eq 0 ]; then
rm -f %{_bindir}/semaphore
fi
%post container
%systemd_post semaphore.service
%posttrans container
ln -sf %{_libexecdir}/semaphore-container %{_bindir}/semaphore
%preun container
%systemd_preun semaphore.service
if [ $1 -eq 0 ]; then
rm -f %{_bindir}/semaphore
fi
%postun container
%systemd_postun semaphore.service
%clean
%{__rm} -rf %{buildroot}
%files
%defattr(-,root,root,-)
%{_sysusersdir}/semaphore.conf
%dir %attr(0750,semaphore,semaphore) %{_sysconfdir}/semaphore
%config(noreplace) %attr(0640,semaphore,semaphore) %{_sysconfdir}/semaphore/config.json
%config(noreplace) %attr(0600,root,root) %{_sysconfdir}/semaphore/env
%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore
%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore/tmp
%files service
%defattr(-,root,root,-)
%{_libexecdir}/semaphore
%ghost %{_bindir}/semaphore
%attr(0644,root,root) %{_unitdir}/semaphore.service
%dir %{_datadir}/containers/systemd/semaphore-db.container.d
%{_datadir}/containers/systemd/semaphore-db.container.d/publish.conf
%files container
%defattr(-,root,root,-)
%attr(0755,root,root) %{_libexecdir}/semaphore-container
%ghost %{_bindir}/semaphore
%{_datadir}/containers/systemd/semaphore.container
%{_datadir}/containers/systemd/semaphore.pod
%dir %{_sysconfdir}/containers/systemd/semaphore.pod.d
%config(noreplace) %{_sysconfdir}/containers/systemd/semaphore.pod.d/enable.conf
%files postgresql
%defattr(-,root,root,-)
%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore/postgresql
# For -service: systemd drop-in switching the native service to PostgreSQL
%dir %{_sysconfdir}/systemd/system/semaphore.service.d
%config(noreplace) %{_sysconfdir}/systemd/system/semaphore.service.d/database.conf
# For -container: semaphore-db container + drop-in wiring it to semaphore
%{_datadir}/containers/systemd/semaphore-db.container
%dir %{_datadir}/containers/systemd/semaphore.container.d
%config(noreplace) %{_datadir}/containers/systemd/semaphore.container.d/database.conf
%dir %{_datadir}/containers/systemd/semaphore-db.container.d
%{_datadir}/containers/systemd/semaphore-db.container.d/pod.conf
%dir %{_sysconfdir}/containers/systemd/semaphore-db.container.d
%config(noreplace) %{_sysconfdir}/containers/systemd/semaphore-db.container.d/credentials.conf
%changelog
* Tue Aug 18 2026 Zoran Pericic <zpericic@netst.org> - 2.19.8-1
- Initial package with service, container and postgresql subpackages
+2
View File
@@ -0,0 +1,2 @@
g semaphore 127
u semaphore 127:127 "Semaphore UI" /var/lib/semaphore /sbin/nologin