Stackvora.tech

Create a Custom systemd Service

Detailed guide on creating robust custom systemd services, covering unit file structure, service types, dependencies, security, and common troubleshooting for Linux systems.

Managing background processes and applications effectively on Linux systems is critical for stability and reliability. systemd, the prevalent init system in modern Linux distributions (e.g., RHEL 7+, Ubuntu 16.04+, Debian 8+), provides a powerful and standardized framework for this. This guide delves into creating a custom systemd service from scratch, covering the essential components, configuration options, and best practices for robust application management.

A systemd service unit file defines how a daemon or application is started, stopped, and managed by the operating system. These files typically reside in /etc/systemd/system/ for custom services, or /usr/lib/systemd/system/ for packages installed by the system's package manager.

Understanding the Unit File Structure

A systemd unit file is an INI-style plain text file divided into sections. The most common sections for service units are [Unit], [Service], and [Install]. Let's break down a typical example, expanding on the provided stub.

# /etc/systemd/system/myapp.service

[Unit]
Description=My Custom Application Service
Documentation=https://docs.example.com/myapp-service
After=network.target syslog.target
Wants=postgresql.service # Example: myapp wants postgresql but doesn't strictly require it to start
Requires=another_critical_service.service # Example: myapp will fail if this critical service doesn't start

[Service]
Type=simple
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID # Send HUP signal for graceful reload
ExecStop=/bin/kill -TERM $MAINPID # Send TERM signal for graceful shutdown
Restart=on-failure
RestartSec=5s
User=myappuser
Group=myappgroup
WorkingDirectory=/var/lib/myapp
Environment="MYAPP_ENV=production" "PORT=8080"
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536 # Increase file descriptor limit
PrivateTmp=true # Isolate /tmp and /var/tmp
ProtectSystem=full # Read-only /usr, /boot, /etc
ProtectHome=true # Isolate /home, /root, /run/user

[Install]
WantedBy=multi-user.target

The [Unit] Section

This section contains generic options about the unit itself, independent of its type.

  • Description: A human-readable description of the service. Appears in systemctl status output.
  • Documentation: Optional URLs for documentation related to the service.
  • After, Before: Define ordering dependencies. After=network.target means this service starts only after the network is up. It implies an ordering but not a strong dependency.
  • Wants, Requires: Define dependency types. Wants is a weaker dependency; if the wanted unit fails, this service can still start. Requires is a stronger dependency; if the required unit fails or is not active, this service will not be started. These also imply ordering.
  • PartOf: Makes this unit part of another unit. If the other unit is stopped, this one is too.
  • BindsTo: A stronger version of Requires; if the bound unit stops, this one also stops.

The [Service] Section

This section defines the behavior specific to services.

  • Type: Specifies the process startup type. Common types include:
    • simple (default): The ExecStart command is the main process. systemd considers the service active as soon as ExecStart is invoked. Use for processes that daemonize themselves or stay in the foreground.
    • forking: ExecStart forks a child process and the parent exits. systemd considers the service started once the parent exits. Requires PIDFile to specify the PID of the main process if systemd can't detect it automatically.
    • oneshot: A command that runs once and then exits. systemd waits for the command to complete. Useful for setup tasks.
    • notify: Similar to simple, but the daemon sends a signal (READY=1) via sd_notify() when it's ready.
    • idle: ExecStart is executed only when all active jobs are dispatched. Useful for services that should not interfere with active requests.
  • ExecStart: The absolute path to the command or script to execute when the service starts.
  • ExecReload: Command to execute when the service is reloaded (systemctl reload myapp.service). Often sends a SIGHUP to the main process.
  • ExecStop: Command to execute when the service is stopped (systemctl stop myapp.service). Usually sends a SIGTERM.
  • ExecStartPre, ExecStartPost, ExecStopPost: Commands run before/after ExecStart or after ExecStop.
  • Restart: Defines when the service should be restarted. Options include no, on-success, on-failure, on-abnormal, on-watchdog, on-abort, always. on-failure is a good default for most applications.
  • RestartSec: How long to wait before restarting the service (e.g., 5s).
  • User, Group: The user and group under which the service process will run. Crucial for security.
  • WorkingDirectory: The working directory for the executed process.
  • Environment, EnvironmentFile: Set environment variables for the service. EnvironmentFile can load variables from an external file.
  • StandardOutput, StandardError: Where to direct stdout/stderr. Options include inherit, null, tty, journal (default), syslog, kmsg, file:/path/to/log. Directing to journal is generally recommended for easy log access via journalctl.
  • LimitNOFILE, LimitNPROC: Set resource limits (e.g., open file descriptors, number of processes).
  • PrivateTmp: Provides a private /tmp and /var/tmp directory for the service, enhancing isolation.
  • ProtectSystem, ProtectHome: Security directives that make parts of the filesystem read-only or inaccessible to the service, significantly reducing attack surface. ProtectSystem=full makes /usr, /boot, /etc read-only. ProtectHome=true makes /home, /root, /run/user inaccessible.

The [Install] Section

This section defines how the service is enabled and disabled.

  • WantedBy, RequiredBy: Specifies the target (runlevel) under which the service should be enabled. multi-user.target is the standard for most server-side services, equivalent to traditional runlevel 3 or 5. When you run systemctl enable myapp.service, a symlink is created from /etc/systemd/system/multi-user.target.wants/myapp.service to the actual unit file.

Service Creation Workflow

  1. Create the Unit File: Place your .service file in /etc/systemd/system/. Ensure it has appropriate permissions (e.g., 644).
  2. Create Application User/Group (if needed): For security, run services under a dedicated, unprivileged user.
    sudo useradd --system --no-create-home --shell /sbin/nologin myappuser
    sudo groupadd --system myappgroup
    sudo usermod -a -G myappgroup myappuser
  3. Reload systemd Daemon: After creating or modifying any unit file, inform systemd about the changes.
    sudo systemctl daemon-reload
  4. Enable the Service: This creates the necessary symlinks so the service starts automatically on boot.
    sudo systemctl enable myapp.service
  5. Start the Service: Immediately start the service.
    sudo systemctl start myapp.service

    You can combine enable and start:

    sudo systemctl enable --now myapp.service
  6. Check Service Status: Verify the service is running correctly and inspect recent logs.
    sudo systemctl status myapp.service
  7. View Logs: Access detailed logs for your service using journalctl.
    sudo journalctl -u myapp.service -f

    The -f flag tails the logs for real-time monitoring.

Common Pitfalls and Troubleshooting

  • Absolute Paths: Always use absolute paths for ExecStart, ExecReload, ExecStop, and any binaries or configuration files referenced. The service's environment might not have the same PATH as your interactive shell.
  • Permissions: Ensure the User and Group specified in the unit file have read/write access to necessary directories and files (e.g., logs, configuration, data). Also, ensure the service unit file itself has correct permissions (e.g., 644).
  • Service Type Mismatch: Using Type=simple for a daemon that forks can lead to systemd thinking the service has exited prematurely. If your application daemonizes, use Type=forking and potentially PIDFile.
  • Dependency Issues: If your service fails to start, check After= and Requires= directives. A common mistake is starting a service before its required database or network interface is fully up. Use systemctl list-dependencies myapp.service to inspect its dependency tree.
  • Environment Variables: Ensure all necessary environment variables are set either directly in the unit file via Environment= or through EnvironmentFile=.
  • Resource Limits: Services might fail if they hit system resource limits (e.g., too many open files). Use LimitNOFILE, LimitNPROC to adjust these. Check ulimit -a in your shell to see current limits, but note that systemd sets its own.
  • Logging: Always configure StandardOutput=journal and StandardError=journal. This centralizes logs and makes debugging easier with journalctl. Avoid writing directly to files unless absolutely necessary, as it complicates log rotation and centralization.
  • Error Messages: journalctl -u myapp.service --since "1 hour ago" -e provides the last few log entries, very useful for quickly identifying startup errors. Pay close attention to error codes from systemctl status. An exit code of 0 usually means success, anything else indicates a problem.

Back to the knowledge base · Ask the AI assistant