Systemd Services for Self-Hosting

What Is systemd?

systemd is the init system and service manager on modern Linux distributions (Ubuntu, Debian, Fedora, Arch). It starts your system, manages background services (daemons), handles logging, and controls the boot process. For self-hosting, systemd ensures your services start automatically after a reboot and restart if they crash.

Docker Compose handles container lifecycle, but systemd manages Docker itself and anything running outside containers — monitoring scripts, backup cron alternatives, VPN clients, and custom automation.

Prerequisites

  • A Linux server running a systemd-based distro (Ubuntu 20.04+, Debian 11+, Fedora 38+)
  • SSH access with sudo privileges (SSH Setup Guide)
  • Basic terminal skills (Linux Basics)

Essential systemctl Commands

systemctl is the command-line tool for interacting with systemd:

# Check if a service is running
systemctl status docker

# Start a service
sudo systemctl start docker

# Stop a service
sudo systemctl stop docker

# Restart a service (stop + start)
sudo systemctl restart docker

# Reload config without full restart (if supported)
sudo systemctl reload nginx

# Enable service to start on boot
sudo systemctl enable docker

# Disable service from starting on boot
sudo systemctl disable docker

# Enable AND start in one command
sudo systemctl enable --now docker

# Check if a service is enabled
systemctl is-enabled docker

# Check if a service is active
systemctl is-active docker

# List all running services
systemctl list-units --type=service --state=running

# List all failed services
systemctl list-units --type=service --state=failed

Understanding Service States

StateMeaning
active (running)Service is running normally
active (exited)Service ran and completed (one-shot)
inactive (dead)Service is stopped
failedService crashed or failed to start
activating (start)Service is in the process of starting
deactivating (stop)Service is shutting down

Check detailed status with:

systemctl status docker
# ● docker.service - Docker Application Container Engine
#      Loaded: loaded (/lib/systemd/system/docker.service; enabled)
#      Active: active (running) since Sun 2026-02-16 08:00:00 UTC; 2h ago
#    Main PID: 1234 (dockerd)
#       Tasks: 45
#      Memory: 128.5M
#         CPU: 1min 23s
#      CGroup: /system.slice/docker.service
#              └─1234 /usr/bin/dockerd

Creating a Custom Service

Service Unit File Structure

Service files live in /etc/systemd/system/ and have three sections:

[Unit]
Description=My Self-Hosted App
Documentation=https://example.com/docs
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/start.sh
ExecStop=/opt/myapp/stop.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Section Breakdown

[Unit] — metadata and dependencies:

DirectivePurpose
DescriptionHuman-readable name shown in systemctl status
AfterStart this service after the listed units
WantsSoft dependency — try to start these, but don’t fail if they don’t
RequiresHard dependency — fail if these aren’t available

[Service] — how to run the service:

DirectivePurpose
Typesimple (default), forking, oneshot, notify
User / GroupRun as this user (never run services as root unless required)
WorkingDirectorySet the working directory before starting
ExecStartThe command to start the service
ExecStopCommand to stop (optional — systemd sends SIGTERM by default)
Restarton-failure, always, on-abnormal, no
RestartSecWait this many seconds before restarting
EnvironmentSet environment variables: Environment=PORT=8080
EnvironmentFileLoad env vars from file: EnvironmentFile=/opt/myapp/.env

[Install] — when to start:

DirectivePurpose
WantedBy=multi-user.targetStart in normal multi-user mode (standard for servers)
WantedBy=graphical.targetStart when GUI is available (desktop systems)

Practical Example: Docker Compose as a systemd Service

If you want your Docker Compose stack to start on boot and restart on failure:

# /etc/systemd/system/myapp-docker.service
[Unit]
Description=MyApp Docker Compose Stack
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=120

[Install]
WantedBy=multi-user.target

Enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp-docker.service

Note: Docker Compose has its own restart policies (restart: unless-stopped), which handle container restarts. This systemd unit is for bringing the whole stack up on boot or after Docker itself restarts.

Practical Example: Backup Script as a Service

Run a backup script with a timer instead of cron:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly Backup Job
After=docker.service

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly at 2 AM

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# Check timer status
systemctl list-timers

Persistent=true means if the server was off at 2 AM, the backup runs as soon as the server starts.

Viewing Logs with journalctl

systemd captures all service output in the journal:

# View logs for a specific service
journalctl -u docker.service

# Follow logs in real time (like tail -f)
journalctl -u docker.service -f

# Show logs since last boot
journalctl -u docker.service -b

# Show logs from the last hour
journalctl -u docker.service --since "1 hour ago"

# Show logs from a specific date
journalctl -u docker.service --since "2026-02-16 08:00" --until "2026-02-16 12:00"

# Show only errors
journalctl -u docker.service -p err

# Show last 50 lines
journalctl -u docker.service -n 50

# Show logs for all Docker containers
journalctl CONTAINER_NAME=mycontainer

# Check disk usage of journal
journalctl --disk-usage

# Trim journal to 500 MB
sudo journalctl --vacuum-size=500M

systemd Timers vs Cron

systemd timers are the modern replacement for cron. Advantages:

FeatureCronsystemd Timer
LoggingMust configure manuallyAutomatic via journalctl
Missed runsSkippedPersistent=true catches up
DependenciesNoneCan require network, Docker, etc.
Random delayNot built-inRandomizedDelaySec built-in
Resource controlNoneFull cgroup support
Status checkingcrontab -lsystemctl list-timers

Recommendation: Use systemd timers for server tasks. Use cron only when you need something simpler or are on a system without systemd (rare for self-hosting).

Common Mistakes

1. Forgetting daemon-reload After Editing Unit Files

After creating or modifying any unit file, you must reload:

sudo systemctl daemon-reload

Without this, systemd uses the cached version and your changes don’t take effect.

2. Using Type=simple for Forking Processes

If your process forks into the background, use Type=forking. With Type=simple, systemd expects the main process to stay in the foreground. If it forks, systemd thinks it exited and marks the service as failed.

3. Not Setting Restart Policies

Without Restart=on-failure, your service won’t restart if it crashes. Always set a restart policy for long-running services:

Restart=on-failure
RestartSec=5

4. Running Services as Root

Never run services as root unless absolutely necessary. Use the User and Group directives:

[Service]
User=appuser
Group=appuser

5. Ignoring Failed Services

Check for failed services regularly:

systemctl --failed

A failed service usually means something needs attention — a config error, missing dependency, or permission issue.

Hardening Services

For services exposed to the network, add these security directives:

[Service]
# Prevent writing to /usr, /boot, /etc
ProtectSystem=strict

# Make /home, /root, /run/user inaccessible
ProtectHome=true

# Private /tmp for this service
PrivateTmp=true

# No access to hardware devices
PrivateDevices=true

# Only allow specific directories to be writable
ReadWritePaths=/opt/myapp/data

# Restrict system calls
SystemCallFilter=@system-service

FAQ

Should I use systemd or Docker restart policies?

Use both. Docker’s restart: unless-stopped handles container crashes. A systemd unit for your Docker Compose stack handles system reboots and ensures the stack starts in the right order relative to Docker itself.

How do I debug a service that won’t start?

Run systemctl status myservice for the immediate error, then journalctl -u myservice -n 50 --no-pager for full logs. Check file permissions on the ExecStart binary and WorkingDirectory.

Can I run Docker Compose stacks without systemd?

Yes — Docker’s restart policies handle most cases. systemd units are useful when you want explicit boot ordering, resource limits, or integration with other systemd services.

How do I limit a service’s memory or CPU usage?

Add resource limits in the [Service] section: MemoryMax=512M and CPUQuota=50%. This uses Linux cgroups under the hood.

Where should I put custom service files?

Always in /etc/systemd/system/. Never modify files in /lib/systemd/system/ — package updates will overwrite them.

Next Steps