systemd Services: Run Your App as a System Service That Survives Every Crash

Your app stopped after you closed SSH? You ran it manually, not as a system service. We write a full unit file line by line, five daily systemctl commands, journalctl logs, and two real examples: a queue worker and a Node service.
The Direct Answer: Make Your App a System Service Managed by systemd
If you started your app with a manual command in the terminal, it is tied to your current SSH session, and it will stop the moment you close it or the connection drops, and it will not come back after a crash or a server reboot. The correct approach on any modern Linux server is to register the app as a systemd service, the core service manager on Ubuntu, Debian, AlmaLinux, and others. Once you write a small unit file and enable it, systemd takes care of starting your app automatically at boot, restarting it immediately if it crashes, collecting its logs in one place, and telling you its status with a single command.
The file itself is only three blocks: a Unit block for the description and dependencies, a Service block for how it runs, and an Install block for when it starts. The most important line in it is Restart=always, which is the difference between a service that revives itself after any crash and an app that stays dead until you discover the failure yourself. In this article we write a full file and explain it line by line, then go through the five commands you use daily, read logs with journalctl, and apply this to two real examples: a queue worker and a Node service serving SSR. We finish with a clear reason why nohup and screen are not a production solution.
Dissecting a Full unit File
Custom service files go in /etc/systemd/system/ with a .service extension. Let us write a service named myapp.service:
| Line | Its job |
|---|---|
| [Unit] | Start of the description and dependency block |
| Description=My App Worker | A description shown in status and logs |
| After=network.target | Do not start until the network is ready |
| [Service] | Start of the how-it-runs block |
| User=deploy | Runs under a limited account, not root |
| WorkingDirectory=/var/www/app | Working directory before starting |
| ExecStart=/usr/bin/php artisan queue:work | The actual command to run, with an absolute path |
| Restart=always | Restart after any exit or crash |
| RestartSec=3 | Wait 3 seconds before each restart |
| [Install] | Start of the boot block |
| WantedBy=multi-user.target | Starts automatically at server boot |
Why Each Line Matters
After=network.target prevents a common race where your app starts before the network is ready and fails to reach the database. User=deploy applies least privilege, so you do not run an internet-facing service as root. ExecStart must use a full absolute path, because systemd does not know your session's PATH variables. As for Restart=always with RestartSec=3, they are the heart of the matter: if your app crashes from an exception or runs out of memory, systemd waits three seconds then brings it back automatically, with no human intervention and no meaningful service downtime.
The Five Daily Commands
After writing the file, tell systemd to reread its files then enable your service:
- sudo systemctl daemon-reload: reload the unit files after any edit to a .service. Many people forget it then wonder why their change did not take effect.
- sudo systemctl enable --now myapp: enable the service for auto-start and run it now, together.
- sudo systemctl restart myapp: restart after deploying a new version of the code.
- sudo systemctl status myapp: show the status, whether it runs, since when, and the last lines of its log.
- sudo systemctl stop myapp: stop it temporarily during maintenance.
These five cover the lifecycle of any service. The golden rule: any edit to a .service file's content is followed by daemon-reload then restart, otherwise systemd stays on the old copy in its memory.
The Logs: journalctl
A big advantage in systemd is that it collects your app's output (stdout and stderr) automatically into a central journal, so you need no manual redirection to files. To read your service's log:
- journalctl -u myapp: the entire service log from the beginning.
- journalctl -u myapp -f: follow the log live moment by moment (like tail -f), useful while debugging.
- journalctl -u myapp --since "1 hour ago": only the last hour's log.
- journalctl -u myapp -p err: errors only at err level and above.
To avoid the disk filling with logs over time, set a limit in /etc/systemd/journald.conf like SystemMaxUse=500M, so the system keeps the last 500 megabytes and deletes the oldest automatically.
Two Real Examples
A Queue Worker
Laravel apps process heavy jobs (sending email, image processing, calling APIs) in a background queue via php artisan queue:work. This command must always run and come back after any crash. The service file uses ExecStart with the command's full path and the right options, like choosing the connection and a per-job timeout. With Restart=always, if the system kills the worker for exceeding memory, systemd brings it back within seconds so jobs do not pile up. An important tip: after each deploy of a new version, restart the service so the worker picks up the new code, because the worker keeps the old code loaded in memory.
A Node or SSR Service
If you run a Node server, or a server-side rendering (SSR) service for a React or Vue app, the same principle applies. ExecStart points to the node binary with an absolute path followed by the entry file, like server.js or the built SSR file. Add environment variables via Environment=NODE_ENV=production or EnvironmentFile=/var/www/app/.env. With the service enabled, your site keeps serving its server-rendered pages without interruption, and revives automatically after a cloud server reboot, which is critical for improving visibility in search engines that favor server-rendered content.
Resource Limits: Protect Your Server From a Runaway Service
Another strong feature in systemd is that it can cap each service's resources, so one service does not consume all the server's memory and take the rest down. Add realistic limits in the Service block:
| Setting | Its job |
|---|---|
| MemoryMax=512M | Stops the service if it exceeds 512 megabytes, instead of devouring the server's memory |
| CPUQuota=50% | Caps CPU use at half of one core |
| TasksMax=100 | Caps the number of child processes to prevent a process explosion |
On an entry server with 2GB of memory, setting MemoryMax per service prevents a memory leak in one service from taking down the whole app. If the service hits the limit, systemd kills it then brings it back immediately under Restart=always, so it recovers automatically instead of hanging the entire server.
systemd Timers Instead of cron
For scheduled tasks, systemd offers an alternative to cron called timer units, with practical advantages. You write two files: a .service file for the task itself, and a .timer file that decides when it runs via OnCalendar in a clear format like OnCalendar=*-*-* 03:00:00 to run daily at three in the morning. Why prefer it over cron in many cases: the task's output goes automatically to journalctl so you read its log like any service, and the Persistent=true option runs a missed task as soon as a server that was off at its scheduled time comes back, instead of silently skipping it as cron does. Suitable for scheduled backups, temporary file cleanup, and critical periodic tasks you do not want to miss.
Why nohup and screen Are Not a Production Solution
Many people run their apps with nohup command & or inside a screen or tmux session and think it is enough. The problem is that none of them restart after a crash: if your app dies from an exception it stays dead until you notice. And none of them revive after a server reboot, so the first kernel update silently drops all your services. And none of them collect logs in an organized, searchable way. In short, nohup and screen are temporary session tools for a quick test, not tools for running a production service. systemd was designed for exactly this role.
| Capability | nohup / screen | systemd |
|---|---|---|
| Restart after crash | No | Yes (Restart=always) |
| Revive after server boot | No | Yes (enable) |
| Central searchable logs | No | Yes (journalctl) |
| Service status in one command | No | Yes (status) |
A Practical Conclusion
To turn any app into a resilient service:
- Write a .service file in /etc/systemd/system/ with three blocks, most important Restart=always and RestartSec=3.
- Run the service under a limited account via User=, not root, and with absolute paths in ExecStart.
- Enable it with enable --now, watch its status with status and its log with journalctl -f.
- After each deploy: daemon-reload then restart so the app picks up the new code.
To run systemd services where you hold full root, you need a cloud server of your own, not shared hosting. Check the cloud server plans and pricing and plans, activation is automatic within seconds after payment on Riyadh servers at roughly 8ms latency.


