A program that must keep running after SSH disconnects needs an execution account, restart policy and log handling. A systemd service unit records those settings together with its start command and boot behavior. Follow a custom service through creation, changes, diagnosis and removal.
Here, we create maru-heartbeat.service, which does not open ports to the outside and only logs periodically. The target is Rocky Linux 9, Ubuntu 24.04·26.04’s system service. The management scope is different from systemctl --user in the user session.
Linux operation articles seen together · Rocky 9 Fixed IP · Ubuntu 24.04 Netplan · Ubuntu 26.04·nmtui·systemd service·log·LVM root extension·general partition root Expand
On this page
1. start and enable are different settings
| command | What you do | Points to Remember |
|---|---|---|
| start / stop | Start/stop the process now | Independent of auto-start on boot setting |
| enable / disable | Register and cancel dependencies such as booting | If –now is not added, the current execution state will not be changed. |
| restart | Stop and restart | There may be brief interruptions. |
| reload | Ask the app to reread its settings | Service must support reload |
| daemon-reload | systemd rereads unit definition | This command does not automatically restart running apps |
disable is not a function that prevents execution. It can be re-executed through manual startup or as a dependency on another unit. A mask that blocks the startup itself has a larger impact area, so identify its purpose and use it. Detailed operation can be checked based on the systemctl manual .
systemctl --version
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service
systemctl --failed
2. Prepare execution account and program
First check if the name below already exists. If you already have the same account or service, choose a different lab name and change the path afterwards as well. Only administrators can change accounts and executable files, and services run under an account with limited privileges.
getent passwd maruops-demo
systemctl cat maru-heartbeat.service
If it has not been created yet, it is expected that the account/unit will not be found in the above command. Proceed as follows only if you are creating a new one.
sudo useradd --system --user-group --no-create-home \
--shell /usr/sbin/nologin maruops-demo
sudoedit /usr/local/bin/maru-heartbeat
#!/usr/bin/bash
set -eu
interval="${HEARTBEAT_INTERVAL:-15}"
case "$interval" in
''|*[!0-9]*)
printf 'HEARTBEAT_INTERVAL must be a positive integer\n' >&2
exit 2
;;
esac
if [ "$interval" -lt 1 ]; then
printf 'HEARTBEAT_INTERVAL must be at least 1\n' >&2
exit 2
fi
trap 'printf "heartbeat stopping\n"; exit 0' TERM INT
while :; do
printf 'heartbeat time=%s interval=%s\n' "$(date --iso-8601=seconds)" "$interval"
sleep "$interval" &
wait "$!"
done
The script leaves the time on standard output and terminates normally when a termination signal is received. & used by Bash to handle signals while waiting for sleep. This is different from a configuration that runs the main process of the actual service in the background and exits systemd.
sudo chown root:root /usr/local/bin/maru-heartbeat
sudo chmod 755 /usr/local/bin/maru-heartbeat
sudo bash -n /usr/local/bin/maru-heartbeat
sudoedit /etc/maru-heartbeat.env
HEARTBEAT_INTERVAL=15
sudo chown root:root /etc/maru-heartbeat.env
sudo chmod 644 /etc/maru-heartbeat.env
In this environment file, only the interval values that can be made public are entered. EnvironmentFile is not a source file like a regular shell script, so no export or arbitrary shell commands are written. Do not assume that secrets such as passwords and tokens are safely hidden only in environment variables; follow the application’s secret management method. Please refer to the configuration description of systemd.exec.
3. Create and register a unit file
sudoedit /etc/systemd/system/maru-heartbeat.service
[Unit]
Description=MaruOps heartbeat demonstration
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=maruops-demo
Group=maruops-demo
EnvironmentFile=/etc/maru-heartbeat.env
ExecStart=/usr/local/bin/maru-heartbeat
Restart=on-failure
RestartSec=3s
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=maru-heartbeat
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
| settings | Reason for choosing |
|---|---|
| Type=exec | Used to treat failure of the step that runs the specified executable as a startup error. |
| User / Group | Do not grant unnecessary root privileges to services |
| Restart=on-failure | Restart in case of abnormal termination. Administrator’s stop is not subject to automatic restart |
| StartLimitIntervalSec / Burst | Limit infinite restarts to short periods of time |
| StandardOutput / Error | View app output in journal |
| ProtectSystem / ProtectHome | This log-only demo restricts access as it does not require modification to the file system or home. |
Just because it is started with Type=exec does not mean that the app’s API or DB connection is ready. To elaborately notify that preparation is complete, review the methods supported by the program, such as notify. In an actual app that writes files, you will need to design StateDirectory or write permission paths, so do not copy and apply the security settings in this demo. Service type/restart policy and unit common settings are the standard documents.
This program doesn’t use any external network, so I didn’t put the network-online.target dependency. After is a start order and is not a function to pull the other unit, and network online judgment does not guarantee a response from a specific DB or Internet service.
sudo chmod 644 /etc/systemd/system/maru-heartbeat.service
sudo systemd-analyze verify /etc/systemd/system/maru-heartbeat.service
sudo systemctl daemon-reload
sudo systemctl enable --now maru-heartbeat.service
systemctl is-enabled maru-heartbeat.service
systemctl is-active maru-heartbeat.service
sudo systemctl status maru-heartbeat.service --no-pager
In Rocky, if the execute permissions are correct but are denied, check the SELinux context. You can restore it to sudo restorecon -v /usr/local/bin/maru-heartbeat according to the default path. Do not fix it by turning off SELinux without determining the cause of the rejection.
4. Choose a reflection method according to the changed object.
| What I changed | reflection order |
|---|---|
| App script/environment file contents | Restart the service |
| unit body·drop-in | After daemon-reload, restart the service |
| Settings in the app itself | Reload if the app supports reload, otherwise restart |
| Runs at boot time | enable/disable; If you want to change now –now |
To change the log interval, change the value of /etc/maru-heartbeat.env to 5 and restart. Since the path or definition of the unit has not changed, daemon-reload is not necessary in this case. We have not implemented the reload behavior in this demo.
sudoedit /etc/maru-heartbeat.env
sudo systemctl restart maru-heartbeat.service
sudo journalctl -u maru-heartbeat.service -n 20 --no-pager
Overwrite some settings with drop-in
When changing the unit provided by a distribution or package, use drop-in rather than directly overwriting the original. You can practice the same method in the demo. Place the following two lines in the editing area of the command below and save. If you write outside the “save area” indicated in the editor, it may not be reflected.
sudo systemctl edit maru-heartbeat.service
[Service]
RestartSec=10s
sudo systemctl daemon-reload
sudo systemctl restart maru-heartbeat.service
systemctl cat maru-heartbeat.service
systemctl show maru-heartbeat.service \
-p FragmentPath -p DropInPaths -p RestartUSec
A typical storage path is /etc/systemd/system/maru-heartbeat.service.d/override.conf. When changing an ExecStart from a drop-in, you must first specify the new ExecStart with the line ExecStart=, which clears the existing list. Before deleting the demo unit you created yourself, double-check the actual path with cat·FragmentPath.
5. View only necessary logs in journalctl
sudo journalctl -u maru-heartbeat.service -n 50 --no-pager
sudo journalctl -u maru-heartbeat.service -f
-f continues waiting for new logs. Ctrl+C ends log viewing and the service continues to run. Run each of the following commands separately as needed.
sudo journalctl -u maru-heartbeat.service --since today --no-pager
sudo journalctl -u maru-heartbeat.service --since '1 hour ago' -o short-iso --no-pager
sudo journalctl -u maru-heartbeat.service -b --no-pager
sudo journalctl --list-boots
sudo journalctl -u maru-heartbeat.service -b -1 --no-pager
sudo journalctl -k -b -p warning --no-pager
-b -1 reads archived logs from previous boots. If there are no previous logs left, they cannot be viewed. -p warning filters by the severity recorded in the journal, so simply writing ERROR in the app’s output sentence does not result in an error level. You can check the meaning of the filter in the journalctl manual.
To keep logs across reboots
sudo journalctl --disk-usage
sudo install -d -m 755 /etc/systemd/journald.conf.d
sudoedit /etc/systemd/journald.conf.d/60-maruops-retention.conf
[Journal]
Storage=persistent
SystemMaxUse=200M
SystemKeepFree=1G
MaxRetentionSec=14day
sudo systemctl restart systemd-journald.service
sudo journalctl --flush
sudo journalctl --disk-usage
This example applies to the entire system journal. Set capacity and retention to match the operating policy. If the size limit is reached before the configured age limit, older logs may be removed sooner. Also check the distribution’s default Storage behavior. Use the journald storage and size-limit documentation to balance retention requirements with available disk space.
If you need to clean up existing logs, store the necessary records externally and then execute them according to the policy, such as sudo journalctl --rotate --vacuum-time=14d. This operation deletes the old archive journal and is irreversible. This is not a command that selects and deletes the logs of just one service.
6. Where to look in a failed state
sudo systemctl status maru-heartbeat.service --no-pager -l
sudo journalctl -u maru-heartbeat.service -b -n 100 --no-pager
systemctl show maru-heartbeat.service \
-p Result -p ExecMainStatus -p NRestarts
| sign | What to check first |
|---|---|
| 203/EXEC | Execution path·permission·shebang·SELinux·line break format |
| 217/USER | Does a User·Group account exist? |
| 200/CHDIR | WorkingDirectory path and access permissions |
| start-limit-hit | Causes of repeat failure and limits on number of restarts |
If the start limit is reached after correcting the cause, initialize the failure status/counter of the unit with sudo systemctl reset-failed maru-heartbeat.service and start again. reset-failed does not fix executables or incorrect settings.
7. Delete the service you created yourself
The following is a procedure for removing only the files you created in this article. Services installed by the package follow the package’s removal/suspension policy. Stop running and auto-starting before deleting files.
sudo systemctl disable --now maru-heartbeat.service
sudo rm -f /etc/systemd/system/maru-heartbeat.service.d/override.conf
sudo rmdir --ignore-fail-on-non-empty /etc/systemd/system/maru-heartbeat.service.d
sudo rm -f /etc/systemd/system/maru-heartbeat.service
sudo systemctl daemon-reload
systemctl list-unit-files maru-heartbeat.service
If you did not create a drop-in, rmdir may output an error because the directory does not exist. Directories containing other files are not forcibly deleted. It is expected that unit is not found in systemctl status. Even if you delete the unit file, past journal or app data is not automatically deleted.
Remove the executable and environment files created just for this demo when they are no longer needed. To delete an account, make sure that no other processes or files are using the account. Since real applications have different lifetimes for executable files, settings, data, and logs, it is best to distinguish between deregistering services and deleting data.
Verification range · 2026-10-18: unit syntax was confirmed in systemd 252 of Rocky Linux 9.8 and systemd 259 of Ubuntu 26.04.1. Service creation/execution, environment file/drop-in reflection, automatic restart after abnormal termination, journal search/permanent storage were performed on systemd 255 of Ubuntu 24.04.5 VM. The demo’s permission restrictions don’t necessarily mean they will fit every app’s file access needs.
Download the network·systemd example file·Netplan YAML, bundled scripts that read service·environment files and storage structures. Change the address and device name to match the actual environment. It does not include scripts to auto-expand disks.



