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.