π³ Install Podman Server¶
Podman is an open-source container engine for developing, managing, and running containers and container images.
Podman provides a Docker-compatible experience while running containers without requiring a central daemon, making it a lightweight and secure alternative for containerized applications.
Features:
- rootless Podman container execution with automatic boot startup via
systemd --userandlinger - one dedicated Linux user per Podman service, each with isolated home/config/cache/data directories
- strict filesystem isolation between service users, with explicit administrative override access for the debian sudoer account
- automatic ownership/group inheritance for newly created service files and directories
- administrative access from the
debiansudoer user to all Podman service files and directories
π₯ Installation¶
Podman can be installed easily on debian system using apt.
# install podman and its requirements
sudo apt update
sudo apt install -y \
podman \
podman-compose \
uidmap \
passt \
aardvark-dns \
slirp4netns \
fuse-overlayfs \
systemd-container \
openssh-server \
skopeo \
jq \
dbus-x11
# install ACL
sudo apt install -y acl
# install apparmor
sudo apt install apparmor apparmor-utils
sudo systemctl enable --now apparmor
# check that podman is installed
podman --version
Install the helper image_userinfo to your shell (run once)
In rootless Podman, a container writes its data as a specific in-container UID/GID, which maps to a host subuid. For a service to read and write its bind-mounted data directories, those directories must be owned by that matching subuid, so the first thing you need to know is which user the image runs as.
The helper below reads an image's config straight from the registry and prints, in a single table:
- the UID/GID to
chownthe data dirs to, - whether to pin
User=in the quadlet, - and whether the image expects
PUID/PGID.
if grep -qF 'image_userinfo()' ~/.bashrc 2>/dev/null; then
echo "image_userinfo() already in ~/.bashrc β skipping"
else
cat >> ~/.bashrc <<'EOF'
# image_userinfo β for a container image, print (for rootless Podman) the uid[:gid] to
# chown its data dirs to, whether to pin User=, and whether it needs PUID/PGID.
# Reads the image config from the registry. For images whose runtime identity isn't in the
# config (named USER, a gosu/su-exec drop, or a docker-entrypoint image with a built-in
# user like redis), it pulls the image once to read /etc/passwd, then removes it again if
# it wasn't already cached.
# Usage: image_userinfo [IMAGE] [UID] (prompts if no IMAGE; UID defaults to 1000)
# Requires: skopeo, jq (and podman for the resolve step)
image_userinfo() {
local img="${1:-}" want="${2:-1000}"
if [ -z "$img" ]; then
read -r -p "Docker image path: " img
[ -z "$img" ] && { echo "error: no image provided" >&2; return 1; }
fi
local t; for t in skopeo jq; do
command -v "$t" >/dev/null 2>&1 || { echo "error: '$t' not found (sudo apt install $t)" >&2; return 1; }
done
local cfg; cfg="$(skopeo inspect --config "docker://${img}" 2>/dev/null)" \
|| { echo "error: cannot read config for '$img'" >&2; return 1; }
local user entry cmd ep0 cmd0 imgenv vendor src maint base cands
user="$(jq -r '.config.User // ""' <<<"$cfg")"
entry="$(jq -r '(.config.Entrypoint // []) | join(" ")' <<<"$cfg")"
cmd="$(jq -r '(.config.Cmd // []) | join(" ")' <<<"$cfg")"
ep0="$(jq -r '(.config.Entrypoint // [])[0] // ""' <<<"$cfg")"
cmd0="$(jq -r '(.config.Cmd // [])[0] // ""' <<<"$cfg")"; cmd0="${cmd0##*/}"
imgenv="$(jq -r '(.config.Env // []) | join(" ")' <<<"$cfg")"
vendor="$(jq -r '.config.Labels["org.opencontainers.image.vendor"] // ""' <<<"$cfg")"
src="$(jq -r '.config.Labels["org.opencontainers.image.source"] // ""' <<<"$cfg")"
maint="$(jq -r '.config.Labels.maintainer // ""' <<<"$cfg")"
base="${img##*/}"; base="${base%%:*}"
cands="$base ${base%%-*} $cmd0 ${cmd0%%-*}" # candidate built-in usernames
local is_s6=false is_lsio=false is_gosu=false is_wrapper=false
case " $entry " in *" /init "*) is_s6=true ;; esac
case " $cmd " in *" /init "*) is_s6=true ;; esac
case "$(printf '%s %s %s' "$vendor" "$src" "$maint" | tr 'A-Z' 'a-z')" in
*linuxserver*) is_lsio=true ;;
esac
case " $imgenv " in *" GOSU_VERSION="*) is_gosu=true ;; esac
case "$ep0" in *docker-entrypoint*) is_wrapper=true ;; esac
# Resolve a real identity from the image's /etc/passwd (pulls if needed, then removes
# the image again only if it wasn't already cached). Looks for: a given username, else
# the entrypoint's gosu/su-exec target, else a built-in user matching the image name.
__iu_resolve() { # <img> <user|''> <entrypoint|''> <candidates> -> "uid[:gid]" ('' if unknown)
command -v podman >/dev/null 2>&1 || return 0
local i="$1" u="$2" ep="$3" cl="$4" present=false line uid gid
podman image exists "$i" 2>/dev/null && present=true
$present || echo " β¦resolving identity from $i (one-time pull)β¦" >&2
line="$(podman run --rm --entrypoint sh "$i" -c '
u="$1"; ep="$2"; cl="$3"
if [ -z "$u" ]; then
[ -n "$ep" ] && [ ! -f "$ep" ] && ep="$(command -v "$ep" 2>/dev/null)"
for f in "$ep" /usr/local/bin/docker-entrypoint.sh /docker-entrypoint.sh /entrypoint.sh; do
[ -f "$f" ] || continue
u="$(grep -hoE "(gosu|su-exec) [A-Za-z0-9_-]+" "$f" 2>/dev/null | awk "{print \$2}" | head -n1)"
[ -n "$u" ] && break
done
fi
if [ -z "$u" ]; then
for c in $cl; do grep -q "^${c}:" /etc/passwd 2>/dev/null && { u="$c"; break; }; done
fi
[ -n "$u" ] || exit 0
grep "^${u}:" /etc/passwd 2>/dev/null || getent passwd "$u" 2>/dev/null
' _ "$u" "$ep" "$cl" 2>/dev/null)"
$present || podman rmi "$i" >/dev/null 2>&1
uid="$(printf '%s' "$line" | cut -d: -f3 | tr -dc '0-9')"
gid="$(printf '%s' "$line" | cut -d: -f4 | tr -dc '0-9')"
[ -z "$uid" ] && return 0
if [ -n "$gid" ] && [ "$gid" != "$uid" ]; then printf '%s:%s' "$uid" "$gid"; else printf '%s' "$uid"; fi
}
local kind quser puid chownto res ru rg
if $is_s6 || $is_lsio; then
kind="LinuxServer/s6"; quser="don't set"; puid="PUID/PGID=${want}"; chownto="${want}"
elif [ -n "$user" ]; then
kind="declares USER ($user)"; quser="not needed"; puid="no"
if [[ "$user" =~ ^[0-9]+(:[0-9]+)?$ ]]; then
chownto="${user}"
else
res="$(__iu_resolve "$img" "${user%%:*}" "$ep0" "$cands")"
chownto="${res:-${user} (verify uid)}"
fi
elif $is_gosu || $is_wrapper; then
res="$(__iu_resolve "$img" "" "$ep0" "$cands")"; puid="no"
if [ -n "$res" ]; then
ru="${res%%:*}"; rg="${res#*:}"; [ "$rg" = "$res" ] && rg="$ru"
$is_gosu && kind="root -> gosu-drop" || kind="root + built-in user"
quser="User=${ru}:${rg}"; chownto="${res}"
else
kind="no USER -> root"; quser="User=${want}:${want}"; chownto="${want}"
fi
else
kind="no USER -> root"; quser="User=${want}:${want}"; puid="no"; chownto="${want}"
fi
local -a H=("Image" "Type" "Quadlet User=" "PUID/PGID" "data_dirs to")
local -a V=("$img" "$kind" "$quser" "$puid" "$chownto")
local -a W=(); local i len
for i in 0 1 2 3 4; do W[i]=${#H[i]}; len=${#V[i]}; [ "$len" -gt "${W[i]}" ] && W[i]=$len; done
__iu_seg() { local n=$(( $1 + 2 )) s=''; while [ "$n" -gt 0 ]; do s+='β'; n=$((n-1)); done; printf '%s' "$s"; }
printf 'β'; for i in 0 1 2 3 4; do printf '%s' "$(__iu_seg "${W[i]}")"; [ "$i" -lt 4 ] && printf 'β¬'; done; printf 'β\n'
printf 'β'; for i in 0 1 2 3 4; do printf ' %-*s β' "${W[i]}" "${H[i]}"; done; printf '\n'
printf 'β'; for i in 0 1 2 3 4; do printf '%s' "$(__iu_seg "${W[i]}")"; [ "$i" -lt 4 ] && printf 'βΌ'; done; printf 'β€\n'
printf 'β'; for i in 0 1 2 3 4; do printf ' %-*s β' "${W[i]}" "${V[i]}"; done; printf '\n'
printf 'β'; for i in 0 1 2 3 4; do printf '%s' "$(__iu_seg "${W[i]}")"; [ "$i" -lt 4 ] && printf 'β΄'; done; printf 'β\n'
unset -f __iu_seg __iu_resolve
}
EOF
echo "image_userinfo() added to ~/.bashrc"
fi
source ~/.bashrc
π Rootless Services¶
Each Podman service runs as its own dedicated Linux user to provide strong isolation between containers, configuration, runtime state, and persistent data.
Example:
n8nβ usern8n_svc, groupn8n_adminsimmichβ userimmich_svc, groupimmich_admins
The debian administrative user belongs to all *_admins groups, allowing centralized management of every service while keeping services isolated from each other. Service users belong only to their own primary group and do not share access with other services.
Each service uses two separate storage locations:
| Path | Purpose |
|---|---|
/media/ssd/podman/<service>/ |
persistent application data, configuration, environment files, and Quadlet definitions |
/media/ssd/podman-users/<service>/ |
rootless Podman user home containing container runtime state, images, volumes, cache, and user configuration |
Example layout:
/media/ssd/podman/service/
βββ Containerfile
βββ postgres.env
βββ service.env
βββ quadlet/
β βββ service.network
β βββ postgres.container
β βββ service.container
βββ data/
βββ service/
βββ postgres-db/
Rootless Podman requires a valid home directory for each service user because Podman stores per-user runtime data under:
/media/ssd/podman-users/<service>/
βββ .config/
βββ .cache/
βββ .local/share/containers/
Container runtime storage is isolated per service user:
~/.local/share/containers/
βββ storage/
βββ overlay/ # container filesystem layers
βββ overlay-images/ # container image metadata
βββ overlay-containers/ # container metadata
βββ volumes/ # named Podman volumes
Each service user therefore has its own isolated:
- container images
- containers
- named volumes
- network namespace
- Podman configuration
- runtime state
βοΈ Configuration¶
π Base Directories¶
Prepare the base directories used to store service data and per-user Podman runtime files. The permissions below keep the directories manageable by the debian administrative user while limiting access for other users.
Create podman-users home directory
# create podman-users home directory
# u=rwX β owner gets:
# - rw for files
# - rwx for directories
# - preserves execute bit only for already executable files
# go=x β group and others get traverse permission on directories only
sudo mkdir -p /media/ssd/podman-users
sudo chown debian:debian /media/ssd/podman-users
sudo chmod u=rwX,go=x /media/ssd/podman-users
Create podman services directory
# create podman services directory
# u=rwX β owner gets:
# - rw for files
# - rwx for directories
# - preserves execute bit only for already executable files
# go=x β group and others get traverse permission on directories only
sudo mkdir -p /media/ssd/podman
sudo chown debian:debian /media/ssd/podman
sudo chmod u=rwX,go=x /media/ssd/podman
Harden debian user home
# harden debian user home
# u=rwX β owner gets:
# - rw for files (600)
# - rwx for directories (700)
# - preserves execute bit only for already executable files
# go= β group and others get no permissions
sudo chmod u=rwX,go= /home/debian
Harden /media/ssd top-level directory
# harden the ssd top-level directory
# u=rwX β owner keeps full access
# go=x β group and others get traverse permission only: every account can
# still pass through to reach its own subdirectory, but none can
# list what else lives on the SSD
sudo chmod u=rwX,go=x /media/ssd
Audit /media/ssd top-level permissions
Run this after adding any new directory on the SSD:
# one line per entry: mode, owner:group, path
sudo find /media/ssd -maxdepth 1 -mindepth 1 -exec stat -c '%A %U:%G %n' {} +
Rule of thumb: the last permission triplet ("others") of every entry should show at most
--x. An account that needs more on a directory (e.g.www-dataon an html directory, the mkdocs container on its bare repo) gets a named ACL entry (setfacl -m u:<user>:...), never world bits β a+after the mode means such entries exist, inspect them withgetfacl.
π₯ User and Group¶
This step creates a dedicated Linux user for the Podman service and a matching administration group for controlled access.
The service runs under its own isolated account, while the debian user is added to the admin group to manage the service files when needed.
Define Service Variables
# define the service name
SERVICE_NAME=XXX
Replace
XXXby your service name
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }
# automatically generate variables
SERVICE_HOME="/media/ssd/podman-users/${SERVICE_NAME}"
SERVICE_USER="${SERVICE_NAME}_svc"
SERVICE_ADMIN="${SERVICE_NAME}_admins"
Create Service User and Access Control
# create Linux user for this service
# --system β create a dedicated system account (non-human service user)
# --create-home β automatically create the user's home directory
# --home-dir β define the service user's home directory location
# --shell β allow local interactive shell access for service administration
# name of the Linux service user to create
sudo useradd \
--system \
--create-home \
--home-dir "${SERVICE_HOME}" \
--shell /bin/bash \
"${SERVICE_USER}"
# restrict SSH access for service users
getent group no-ssh >/dev/null || sudo groupadd no-ssh # create no-ssh group only if missing
id -nG "${SERVICE_USER}" | grep -qw no-ssh || # add service user to no-ssh group only if not already a member
sudo usermod -aG no-ssh "${SERVICE_USER}"
sudo install -d -m 755 /etc/ssh/sshd_config.d # ensure the drop-in directory exists
SSHD_CONFIG="/etc/ssh/sshd_config.d/99-deny-service-users.conf"
sudo tee "${SSHD_CONFIG}" >/dev/null <<EOF # enforce SSH deny rule for members of no-ssh
DenyGroups no-ssh
EOF
sudo sshd -t # test SSH configuration
sudo systemctl reload ssh # reload SSH daemon configuration
# create Linux group for this service and allow debian user to access it
sudo groupadd "${SERVICE_ADMIN}"
sudo usermod -aG "${SERVICE_ADMIN}" debian
Making XDG_RUNTIME_DIR available automatically per user
XDG_RUNTIME_DIR points to the per-user runtime directory (/run/user/
# make it persistent: set it automatically on every future login
sudo tee -a "$(getent passwd "${SERVICE_USER}" | cut -d: -f6)/.profile" >/dev/null <<'EOF'
# Provide XDG_RUNTIME_DIR for rootless/user services when missing
if [ -z "${XDG_RUNTIME_DIR}" ]; then
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
fi
EOF
Notify: re-login required for group changes
# logout and back for group membership to apply
echo "Log out and back in later for ${SERVICE_ADMIN} group membership to apply in interactive shells."
exit
π Persistent Services¶
Rootless Podman containers run inside the service user's systemd --user session.
Without lingering enabled, user services may only start after the user logs in and may stop after logout.
Define Service Variables
# define the service name
SERVICE_NAME=XXX
Replace
XXXby your service name
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }
# automatically generate variables
SERVICE_HOME="/media/ssd/podman-users/${SERVICE_NAME}"
SERVICE_DIR="/media/ssd/podman/${SERVICE_NAME}"
SERVICE_USER="${SERVICE_NAME}_svc"
SERVICE_ADMIN="${SERVICE_NAME}_admins"
Enable Persistent User Services with Lingering
# retrieve the service user's numeric UID
SERVICE_UID="$(id -u "${SERVICE_USER}")"
# allow the user's systemd --user manager to run without interactive login
sudo loginctl enable-linger "${SERVICE_USER}"
# immediately start the user's systemd instance
sudo systemctl start "user@${SERVICE_UID}.service"
π Detecting a container's runtime UID/GID¶
Detecting a container's runtime UID/GID
Once installed, run image_userinfo to see how an image runs and what its data directory permissions should be.
image_userinfo # prompts for the image
image_userinfo docker.io/library/postgres:16-alpine # with docker image argument
It reads the image config from the registry and prints a single table:
debian@odroid:~$ image_userinfo docker.io/library/postgres:16-alpine
β¦resolving uid from docker.io/library/postgres:16-alpine (one-time pull)β¦
+--------------------------------------+-------------------+---------------+-----------+--------------+
| Image | Type | Quadlet User= | PUID/PGID | data_dirs to |
+--------------------------------------+-------------------+---------------+-----------+--------------+
| docker.io/library/postgres:16-alpine | root -> gosu-drop | User=70:70 | no | 70 |
+--------------------------------------+-------------------+---------------+-----------+--------------+
Reading the table
| Column | Meaning |
|---|---|
| Image | The image reference that was inspected. |
| Type | How the image sets its user β declares USER (name) (runs as that user), no USER -> root (runs as root), or LinuxServer/s6 (drops to PUID/PGID). |
| Quadlet User= | What to put in the .container file: a User=uid:gid line to pin, not needed (image already sets it), or don't set (s6 images must stay root). |
| PUID/PGID | Whether to set PUID/PGID in the service's env file β only LinuxServer/s6 images use them. |
| data_dirs to | The uid/gid that must own the bind-mounted data directory β i.e. the value to use in your data_dirs list. |
ποΈ Podman Control¶
podmanctl is a single-file Python tool that gives the debian admin account a console dashboard and lifecycle control over every rootless Podman Quadlet service on the server.
Each service runs in its own isolated userspace where a plain podman ps from debian sees nothing; podmanctl bridges that gap with a single command.
Features:
- list the table of all containers from all service users, with health status and systemd unit state
- start / stop / restart / update any service stack from the
debianaccount, in dependency order β start/stop/restart/git-pull act on all services at once when no service is given - update a service end to end: stop β
git pull --rebaseits repository β rebuild/re-pull images β start - check against the registries (
skopeo) to see which images have a newer version available β local builds are checked through the base images of their DockerfileFROMlines - git pull (
git pull --rebase) one service data directory under/media/ssd/podman/, or every git repository at once when no service is given - fully dynamic discovery: there is no configuration file to maintain; everything is derived live from the conventions:
- services: every subdirectory of
/media/ssd/podman-users/that has a matching<name>_svcLinux user - units, containers, images: parsed from the
*.containerand*.buildQuadlet files in each service's~/.config/containers/systemd/, which thedebianaccount can read sudo-less thanks to the<name>_adminsgroup ACLs - start/stop order: a topological sort of the
After=/Requires=relations between the service's own units - live state:
podman psandsystemctl --userexecuted inside each service user's session (sudo -u <user> env HOME=<home> XDG_RUNTIME_DIR=/run/user/<uid> ...)
- services: every subdirectory of
Installing a new service the usual way (new
<name>_svcuser + Quadlets) makes it appear in the dashboard automatically.
βοΈ Install the script¶
Install podmanctl to /usr/local/bin
# download the script published by this documentation site
sudo curl -fsSL https://docs.fum-server.fr/files/podmanctl.py -o /usr/local/bin/podmanctl
# make it executable for everyone, writable by root only
sudo chmod 0755 /usr/local/bin/podmanctl
# check the installation
podmanctl --help
The script is plain Python 3 standard library β no
pippackage is needed.
π Usage¶
π Dashboard¶
List services with podmanctl --list
podmanctl
SERVICE USER CONTAINER IMAGE STATUS UNIT
calibre calibre_svc calibre-server localhost/calibre:7.21.0 Up 2 days (healthy) active
gokapi gokapi_svc gokapi-server docker.io/f0rc3/gokapi:latest Up 2 days (healthy) active
immich immich_svc immich-postgres ghcr.io/immich-app/postgres:14-vec⦠Up 2 days (healthy) active
immich immich_svc immich-redis docker.io/redis:6.2-alpine Up 2 days (healthy) active
immich immich_svc immich-server localhost/immich:v2.7.5 Up 2 days (healthy) active
planka planka_svc planka-postgres docker.io/library/postgres:16-alpine Up 5 hours (healthy) active
planka planka_svc planka-server ghcr.io/plankanban/planka:2.0.0-rc.4 Exited (1) 3 min ago failed
...
Check for available updates with podmanctl --list --check
podmanctl --list --check
SERVICE USER CONTAINER IMAGE STATUS UNIT UPDATE
gokapi gokapi_svc gokapi-server docker.io/f0rc3/gokapi:latest Up 2 days (healthy) active update available
immich immich_svc immich-postgres ghcr.io/immich-app/postgres:14-vec⦠Up 2 days (healthy) active up-to-date
immich immich_svc immich-redis docker.io/redis:6.2-alpine Up 2 days (healthy) active up-to-date
immich immich_svc immich-server localhost/immich:v2.7.5 Up 2 days (healthy) active up-to-date
...
Images produced by a
.buildQuadlet are checked through their DockerfileFROMbase images (resolved from the Quadlet's build args): the bases were pulled into the service user's storage at build time, so if a base moved on the registry the build is reported asupdate available. Local sources (COPY'd code, git content) are not part of the comparison.
π Start / Stop / Restart¶
Start/Stop/Restart service with podmanctl --<action> <service>
podmanctl --start immich # daemon-reload, then start (databases first)
podmanctl --stop immich # stop the stack (server first, databases last)
podmanctl --restart immich # stop + start
Start/Stop/Restart ALL services with podmanctl --<action>
Omit the service name to apply the action to every detected service, with a summary table at the end:
podmanctl --restart
Stopping calibre ...
$ (calibre_svc) systemctl --user stop calibre-server.service
Starting calibre ...
$ (calibre_svc) systemctl --user daemon-reload
$ (calibre_svc) systemctl --user start calibre-server.service
...
SERVICE RESULT
calibre β
gokapi β
immich β
planka β
...
β done β β error (the failing unit is visible in the live output above the table).
β¬οΈ Update a service¶
Update a service with podmanctl --update <service>
--update runs, in order: stop the stack β git pull --rebase the service repository β rebuild local images / re-pull registry images β start the stack. The service stays down for the whole rebuild, and a failed git pull aborts the update and leaves the service stopped.
# 1. bump the version in the service env file
sudo nano /media/ssd/podman/immich/immich.env # IMMICH_VERSION=v2.8.0
# 2. stop, git pull the source, rebuild the images and restart
podmanctl --update immich
# nothing to edit: stop, pull the new :latest and restart
podmanctl --update n8n
A service whose data directory is not a git repository simply skips the git pull step (β) and updates its images as usual.
β¬οΈ Pull git repositories¶
Pull service git repositories with podmanctl --git-pull [service]
podmanctl --git-pull # every service repository (all at once)
$ git -C /media/ssd/podman/calibre pull --rebase
$ git -C /media/ssd/podman/immich pull --rebase
$ git -C /media/ssd/podman/planka pull --rebase
...
SERVICE RESULT
calibre β
gokapi β
immich β
planka β
...
podmanctl --git-pull immich # a single service repository
$ git -C /media/ssd/podman/immich pull --rebase
SERVICE RESULT
immich β
Omit the service name to pull every repository; pass a
<service>to pull only that one. Repositories are detected as/media/ssd/podman/<name>/.gitand pulled withgit pull --rebaseas thedebianaccount β no sudo involved. β new commits pulled β β nothing done (already up to date, or not a git repository) β β pull failed (the git error is printed below the table).
π Logs¶
Follow a service's journal with podmanctl --logs <service>
podmanctl --logs immich # sudo journalctl _UID=<immich_svc uid> -n 50 -f
ποΈ Removing a Service¶
Removing a service reverses its setup:
- stops the running stack
- deletes the dedicated Linux user
- delete its admin group and rootless ID mappings
- erases both the per-user Podman home and the service data directory
Define Service Variables
# define the service name
SERVICE_NAME=XXX
Replace
XXXby your service name
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }
# automatically generate variables
SERVICE_HOME="/media/ssd/podman-users/${SERVICE_NAME}"
SERVICE_DIR="/media/ssd/podman/${SERVICE_NAME}"
SERVICE_USER="${SERVICE_NAME}_svc"
SERVICE_ADMIN="${SERVICE_NAME}_admins"
Removing a service entirely
# show the absolute path and size of each directory to be deleted
echo "The following directories will be permanently deleted for '${SERVICE_NAME}':"
for dir in "${SERVICE_HOME}" "${SERVICE_DIR}"; do
if [ -d "${dir}" ]; then
printf ' %s (%s)\n' "${dir}" "$(sudo du -sh "${dir}" 2>/dev/null | cut -f1)"
else
printf ' %s (not found)\n' "${dir}"
fi
done
# ask for confirmation, abort otherwise (read from the terminal, not the pasted input)
read -r -p "Type 'yes' to confirm permanent removal: " CONFIRM </dev/tty
[ "${CONFIRM}" = "yes" ] || { echo "Aborted, nothing was removed."; exit 1; }
# stop the service stack from the debian admin account
podmanctl --stop "${SERVICE_NAME}"
# retrieve the service user's numeric UID (needed to stop its systemd --user instance)
SERVICE_UID="$(id -u "${SERVICE_USER}")"
# disable lingering so the user's systemd --user manager no longer starts at boot
sudo loginctl disable-linger "${SERVICE_USER}"
# stop the service user's systemd --user instance (kills any container still running)
sudo systemctl stop "user@${SERVICE_UID}.service"
# remove the service user (its home directory is deleted explicitly below)
sudo userdel "${SERVICE_USER}"
# remove the per-user Podman home and the service data directory
sudo rm -rf "${SERVICE_HOME}" "${SERVICE_DIR}"
# remove the admin group (debian's membership is dropped with it)
sudo groupdel "${SERVICE_ADMIN}"
# remove the rootless subuid/subgid mappings (in case userdel left them behind)
sudo sed -i "/^${SERVICE_USER}:/d" /etc/subuid /etc/subgid
The shared
no-sshgroup and the/etc/ssh/sshd_config.d/99-deny-service-users.confdeny rule are left untouched, they are used by every service user.