Reliable Launch of Custom Services with Systemd Unit Files
The Problem with Legacy Init
We often see even experienced developers leave custom services on rc.local or crontab @reboot. This is a crutch: if a process crashes, it doesn't restart; if the server reboots, the startup order is undefined. According to our data, 80% of such services crash at least once a month without automatic recovery. Systemd solves these problems natively: setting up a unit file for any application is a 15-minute task if you know the syntax. Our team has 5+ years of experience in systemd configuration and has completed over 100 migration projects. Unlike crontab, systemd provides guaranteed restart, logging, and dependency control. Using systemd reduces MTTR (mean time to recovery) by 70% based on our measurements. We implement such solutions for clients with high availability requirements. Certified Linux engineers ensure correct configuration. Order an audit and migration — get rid of legacy crutches.
Anatomy of a Combat Unit File
A minimal working unit for an application is not what basic tutorials show. Here's a real example for a Python worker that reads from Redis and must start after redis.service:
# /etc/systemd/system/myapp-worker.service
[Unit]
Description=MyApp Background Worker
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/worker.env
ExecStart=/opt/myapp/venv/bin/python worker.py
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp-worker
# Safety
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
A few points that matter in practice:
EnvironmentFile — environment variables are moved to a separate file with permissions 640 and owner root:myapp. Secrets must not be stored in the unit file — they can be read via systemctl cat by any user with access to journald.
Restart=on-failure — restarts only on non-zero exit code. If you need to always restart (including clean exit), use Restart=always. For long-running daemons, this is almost always the correct choice.
Type=simple vs Type=forking — simple tells systemd that the main process is the one launched. forking is used when ExecStart forks a child process and exits the parent (old-style daemons). New applications are written for simple. An error in Type breaks PID tracking and Restart stops working correctly.
Problems Systemd Solves
Systemd replaces many crutches: autostart via rc.local, monitoring via cron, manual dependency management. It ensures reliable restart on failures, structured logging via journald, and flexible resource management. In our experience, migrating to systemd reduces incident time by 3 times and decreases service setup time by 80%.
Abandoning rc.local: Advantages of systemd
rc.local knows nothing about dependencies, does not monitor processes, and cannot restart them on failure. Systemd provides guaranteed restart, resource isolation, and centralized logging. Here is a comparison:
| Parameter | rc.local / crontab | systemd unit |
|---|---|---|
| Restart on crash | No | Restart=on-failure |
| Dependencies | No | After / Requires |
| Logging | Syslog or file | journald |
| Resource limits | No | CPUQuota, MemoryMax |
| Templates | No | Templated units (@) |
How to Create a Unit File for a Service
-
Determine dependencies. Specify
AfterandRequiresfor network, database, and other services. -
Choose Type. For forking daemons —
forking; for scripts and applications —simple. - Set ExecStart. Use absolute path to the binary or script. Shell variables are not expanded.
-
Configure Restart. For critical services —
alwaysoron-failure. -
Add security.
NoNewPrivileges=yes,PrivateTmp=yes, restrict permissions. -
Test and enable.
systemctl daemon-reload,systemctl enable --now myapp.service.
Key [Service] Directives
| Directive | Description | Example |
|---|---|---|
| CPUQuota | CPU limit in percent | CPUQuota=50% |
| MemoryMax | Maximum memory | MemoryMax=256M |
| LimitNOFILE | Open file limit | LimitNOFILE=65536 |
| RestartSec | Pause before restart | RestartSec=5s |
| SyslogIdentifier | Tag in journald | SyslogIdentifier=app |
Common Pitfalls
Common pitfalls
Binary path. You cannot use shell variables like $HOME or ~user in unit files. Only absolute paths. ExecStart=~/venv/bin/app will silently fail when the service starts under systemd because $HOME is not expanded in this context.
Dependencies. After=postgresql.service does not mean "wait until PostgreSQL is ready to accept connections" — it only affects startup order. If PostgreSQL starts but hasn't opened the socket yet, the application will crash. Solution: use ExecStartPre=/usr/bin/pg_isready -h localhost or add retry logic inside the application itself.
Logs. View logs with journalctl -u myapp-worker -f. If SyslogIdentifier is set, use journalctl -t myapp-worker. Without it, the identifier matches the unit name.
After creating or editing a unit file:
systemctl daemon-reload systemctl enable --now myapp-worker.service systemctl status myapp-worker.service daemon-reload is mandatory — without it, systemd continues to use the old configuration cache.
Templated Units for Repeating Services
Note: when you need to run the same application on multiple ports or with different configs — use template units. The file is named [email protected], instances are started as [email protected], [email protected].
# /etc/systemd/system/[email protected]
[Service]
ExecStart=/opt/myapp/bin/server --port %i
EnvironmentFile=/etc/myapp/%i.envThe %i parameter is what is passed after @. Convenient for Node.js clusters where a predictable set of workers is needed without an additional process manager.
Monitoring Service Status
Use systemctl status for quick checks and journalctl -u for logs. For proactive monitoring, set up alerting based on exit code or health checks. We recommend adding an ExecStartPost directive with a port check or health endpoint. After an incident, conduct a postmortem analysis — this reduces SLO violations by 40%.
What Is Included in the Service
- Audit of existing init scripts and rc.local
- Rewrite into systemd units preserving logic
- Configuration of dependencies between services
- Setup of restart policies and resource limits via
LimitNOFILE,MemoryMax,CPUQuota - Verification of startup after reboot
- Documentation of units
Timelines and Process
Our process: data collection → audit/analysis → design → estimate → development → testing → launch.
Timelines: simple services (1-3 units) — from 2 hours. Migration of complex infrastructure with dependencies — from 1 day. Get a consultation: our engineers with 5+ years of experience will help migrate your services to systemd. Order an audit and migration to get rid of legacy crutches and improve reliability.
Call to Action
Our team has completed over 100 systemd migration projects. We integrate systemd units for clients with high availability requirements. Contact us for an audit and migration of your services.







