What you will learn
Write a provisioning script that creates a system user, its directories, and a systemd unit — safely re-runnable, so applying it a second time changes nothing instead of erroring or duplicating state.
Before you begin
Requires root. Idempotency here means every step checks "does this already match the desired state?" before acting, rather than assuming a clean slate.
1. Create the user only if it doesn't exist
#!/usr/bin/env bash
set -Eeuo pipefail
SVC_USER="myappsvc"
SVC_HOME="/opt/myapp"
SVC_GROUP="myappsvc"
if ! getent group "$SVC_GROUP" >/dev/null; then
groupadd --system "$SVC_GROUP"
echo "created group ${SVC_GROUP}"
fi
if ! id "$SVC_USER" >/dev/null 2>&1; then
useradd --system --gid "$SVC_GROUP" --home-dir "$SVC_HOME" \
--shell /usr/sbin/nologin --no-create-home "$SVC_USER"
echo "created user ${SVC_USER}"
else
echo "user ${SVC_USER} already exists, skipping"
fi
Checking getent/id before groupadd/useradd avoids the non-zero exit those commands return on "already exists" — without the check, a second run would abort under set -e.
2. Directories and permissions, applied not just created
for dir in "$SVC_HOME" "$SVC_HOME/data" "$SVC_HOME/log"; do
mkdir -p "$dir"
chown "${SVC_USER}:${SVC_GROUP}" "$dir"
chmod 750 "$dir"
done
chown/chmod run unconditionally every time — they're naturally idempotent (setting the same owner/mode twice is a no-op), so there's no need to check current state first, unlike creation commands.
3. Install a systemd unit and reload only if it changed
UNIT_PATH="/etc/systemd/system/myapp.service"
UNIT_CONTENT="$(cat <<EOF
[Unit]
Description=MyApp service
After=network.target
[Service]
Type=simple
User=${SVC_USER}
Group=${SVC_GROUP}
ExecStart=${SVC_HOME}/bin/myapp
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
)"
if [[ ! -f "$UNIT_PATH" ]] || [[ "$(cat "$UNIT_PATH")" != "$UNIT_CONTENT" ]]; then
printf '%s\n' "$UNIT_CONTENT" > "$UNIT_PATH"
systemctl daemon-reload
echo "unit file installed/updated"
else
echo "unit file unchanged"
fi
systemctl enable --now myapp.service
Comparing rendered content against the file on disk before writing avoids an unconditional daemon-reload on every run — cheap here, but the pattern matters more for units systemd re-evaluates expensively.
Example output
$ ./provision.sh
created group myappsvc
created user myappsvc
unit file installed/updated
$ ./provision.sh
user myappsvc already exists, skipping
unit file unchanged
Verify the result
Run the script twice in a row and confirm the second run reports "already exists" / "unchanged" everywhere, with systemctl status myapp showing the service active after both runs.
Troubleshooting
If useradd fails with "home directory already exists," pass --no-create-home when the directory is managed separately (as above) — this is intentional, not an error to suppress.
Next steps
Call this from multi-env-deploy-orchestrator as the first stage of a deploy, so a fresh host provisions itself identically to an existing one.