概述

在 Ubuntu 中,系统服务(systemd service) 是让程序在后台运行、随系统启动、崩溃后自动重启的标准方式。无论你是要运行一个 Web 应用、一个定时脚本、还是一个消息队列消费者,用 systemd 管理它都是最正确的方式。

简单来说:你想让一个程序开机自动运行、出错自动恢复、还能用 systemctl 优雅管理?把它写成 systemd 服务就完事了。

你的脚本/程序 ──▶ 编写 .service 文件 ──▶ systemctl enable ──▶ 开机自启
                    │                          │
                    ├── 定义如何启动           ├── 手动管理:start/stop/restart
                    ├── 定义依赖关系           ├── 查看状态:status
                    └── 定义重启策略           └── 查看日志:journalctl

核心概念

概念 说明
Unit 文件 systemd 的配置单元,.service 是最常用的一种
Service 定义如何启动、停止一个程序
Target 相当于"运行级别",如 multi-user.target 是多用户命令行模式
WantedBy 当前服务应该在哪一个 target 下启动
ExecStart 启动服务的命令
ExecStop 停止服务的命令(可选)
Restart 进程退出后的重启策略

创建你的第一个服务

1. 编写一个简单的测试脚本

先创建一个会被服务管理的小脚本,用来演示:

# 创建一个简单的 Python HTTP 服务作为例子
sudo mkdir -p /opt/myapp

创建 /opt/myapp/app.py

#!/usr/bin/env python3
"""
一个简单的 HTTP 服务,用于演示 systemd 服务管理。
访问 http://localhost:8080 查看状态。
"""
import http.server
import socket
import signal
import sys
import time

PORT = 8080

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(f"""
=== Systemd 服务演示 ===

服务状态: 运行中
主机名: {socket.gethostname()}
时间: {time.strftime('%Y-%m-%d %H:%M:%S')}
PID: {str(self.server.server_address)}

访问 /stop 停止服务(测试 Restart 策略)
""".encode('utf-8'))

    def log_message(self, format, *args):
        """将请求日志输出到 stdout,systemd 会自动捕获到 journal"""
        sys.stdout.write(f"[{self.log_date_time_string()}] {self.address_string()} - {format % args}\n")
        sys.stdout.flush()

if __name__ == '__main__':
    server = http.server.HTTPServer(('0.0.0.0', PORT), Handler)
    print(f"服务已启动,监听端口 {PORT},PID: {server.server_address[1]}")
    print(f"日志查看: journalctl -u myapp.service -f")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("收到停止信号,服务关闭...")
        server.shutdown()
# 添加执行权限
sudo chmod +x /opt/myapp/app.py

2. 创建 Service Unit 文件

Systemd 的 service 文件放在以下位置(按优先级从低到高):

路径 说明
/lib/systemd/system/ 软件包安装的默认服务文件(不要修改)
/etc/systemd/system/ 用户自定义服务文件(推荐)
/etc/systemd/system/ 下的 .wants/ 目录 符号链接,用于 enable/disable

创建 /etc/systemd/system/myapp.service

[Unit]
Description=My Application Service
Documentation=https://example.com/docs
After=network.target network-online.target
Wants=network-online.target

[Service]
# ---- 程序类型 ----
# simple: 主进程直接在前台运行(默认)
# forking: 程序启动后会 fork 到后台(传统 daemon)
# oneshot: 执行一次就退出(用于初始化脚本)
Type=simple

# ---- 启动与停止 ----
# 启动命令(必须使用绝对路径)
ExecStart=/usr/bin/python3 /opt/myapp/app.py

# 停止命令(可选,默认发送 SIGTERM)
ExecStop=/bin/kill -s TERM $MAINPID

# 重载命令(可选)
ExecReload=/bin/kill -s HUP $MAINPID

# ---- 工作目录 ----
WorkingDirectory=/opt/myapp

# ---- 重启策略 ----
# on-failure: 非正常退出时重启
# always: 任何退出都重启
# on-abnormal: 被信号终止时重启
Restart=on-failure

# 重启间隔(秒)
RestartSec=5

# 最大重启次数(0 = 不限制)
StartLimitInterval=60
StartLimitBurst=3

# ---- 安全与隔离 ----
# 以指定用户运行(强烈推荐!不要用 root)
User=nobody
Group=nogroup

# 日志标准输出捕获到 journal
StandardOutput=journal
StandardError=journal

# 环境变量
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=-/etc/myapp/env.conf

[Install]
# 多用户模式下启动(最常用)
WantedBy=multi-user.target

# 也可写成:
# Alias=myapp.service

3. 启动并管理服务

# ---- 重新加载 systemd 配置(每次修改 .service 文件后必须执行) ----
sudo systemctl daemon-reload

# ---- 启动服务 ----
sudo systemctl start myapp

# ---- 设置开机自启 ----
sudo systemctl enable myapp

# 输出:
# Created symlink /etc/systemd/system/multi-user.target.wants/myapp.service → /etc/systemd/system/myapp.service

# ---- 查看服务状态 ----
sudo systemctl status myapp

# 输出示例:
# ● myapp.service - My Application Service
#      Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled)
#      Active: active (running) since Thu 2026-05-28 10:23:45 CST; 2min ago
#    Main PID: 12345 (python3)
#       Tasks: 1 (limit: 2345)
#      Memory: 12.3M
#         CPU: 0.5s
#      CGroup: /system.slice/myapp.service
#              └─12345 /usr/bin/python3 /opt/myapp/app.py
#
# May 28 10:23:45 ubuntu-server systemd[1]: Started My Application Service.

# ---- 验证服务正常运行 ----
curl http://localhost:8080

# ---- 查看日志 ----
sudo journalctl -u myapp.service -f

# ---- 停止服务 ----
sudo systemctl stop myapp

# ---- 重启服务 ----
sudo systemctl restart myapp

# ---- 关闭开机自启 ----
sudo systemctl disable myapp

常用命令速查

# ===== 服务管理 =====
sudo systemctl start   myapp      # 启动服务
sudo systemctl stop    myapp      # 停止服务
sudo systemctl restart myapp      # 重启服务
sudo systemctl reload  myapp      # 重载配置(需 ExecReload)
sudo systemctl status  myapp      # 查看详细状态

# ===== 开机自启管理 =====
sudo systemctl enable  myapp      # 启用开机自启
sudo systemctl disable myapp      # 关闭开机自启
sudo systemctl is-enabled myapp   # 检查是否开机自启
sudo systemctl list-unit-files --type=service --state=enabled  # 查看所有开机自启服务

# ===== systemd 配置重载 =====
sudo systemctl daemon-reload      # 重载所有 unit 文件(修改后必须执行)
sudo systemctl daemon-reexec      # 重载 systemd 自身

# ===== 日志查看 =====
sudo journalctl -u myapp           # 查看该服务的所有日志
sudo journalctl -u myapp -f        # 实时跟踪日志
sudo journalctl -u myapp --since "1 hour ago"  # 查看最近 1 小时日志
sudo journalctl -u myapp -n 50     # 查看最近 50 条日志
sudo journalctl -u myapp -p err    # 只查看错误级别日志

# ===== 系统服务概览 =====
systemctl list-units --type=service          # 查看所有运行中的服务
systemctl list-units --type=service --all    # 查看所有服务(含未启动的)
systemctl list-unit-files --type=service     # 查看所有服务文件及启用状态

# ===== 按状态筛选 =====
systemctl list-units --type=service --state=running    # 运行中的
systemctl list-units --type=service --state=failed     # 失败的
systemctl list-units --type=service --state=exited     # 已退出的

配置详解

[Unit] 段 —— 元数据与依赖

[Unit]
# 服务描述(status 命令会显示这个)
Description=My Application Service

# 文档链接(可选)
Documentation=https://example.com/docs
Documentation=man:myapp(1)

# 依赖关系 —— 本服务在哪些服务之后启动
After=network.target network-online.target
After=postgresql.service
After=redis.service

# 弱依赖 —— 希望这些服务也启动,但不强制
Wants=network-online.target
Wants=postgresql.service

# 强依赖 —— 这些服务必须启动,否则本服务启动失败
Requires=postgresql.service
# Requires 与 After 通常成对使用:
# After 保证启动顺序,Requires 保证依赖存在

# 冲突 —— 不能与这些服务共存
Conflicts=apache2.service

[Service] 段 —— 核心行为

[Service]
# ===== 服务类型 =====
# simple:     ExecStart 是主进程,foreground 运行(默认)
# exec:       类似 simple,但 systemd 会在 fork+exec 后才认为启动完成
# forking:    传统 daemon,启动后 fork 到后台,父进程退出
# oneshot:    执行一次即退出,常用于初始化脚本
# dbus:       等待 D-Bus 名称注册后才认为启动完成
# notify:     通过 sd_notify() 通知 systemd 启动完成
Type=simple

# ===== 启动命令 =====
ExecStart=/usr/bin/python3 /opt/myapp/app.py
# 注意:ExecStart 必须用绝对路径,不支持 ~、管道、重定向

# 停止命令(默认发送 SIGTERM)
ExecStop=/bin/kill -s TERM $MAINPID

# 重载命令(发送 SIGHUP 优雅重载配置)
ExecReload=/bin/kill -s HUP $MAINPID

# 启动前执行的命令(可选)
ExecStartPre=/bin/bash -c "echo '准备启动...'"

# 启动后执行的命令(可选)
ExecStartPost=/bin/bash -c "echo '启动完成'"

# 停止前执行的命令(可选)
ExecStopPost=/bin/bash -c "echo '服务已停止'"

# ===== 重启策略 =====
Restart=on-failure
# 可选值:
#   no:          不重启(默认)
#   on-success:  退出码为 0 时不重启,非 0 时重启
#   on-failure:  退出码非 0、被信号终止、超时时重启(最常用)
#   on-abnormal: 被信号终止、超时时重启(正常退出不重启)
#   on-abort:    仅被未捕获的信号终止时重启
#   always:      任何退出原因都重启

RestartSec=5         # 重启等待间隔(秒)

# 限制重启频率
StartLimitInterval=60     # 时间窗口(秒)
StartLimitBurst=3         # 在此窗口内最多重启次数
# 以上配置:60 秒内最多重启 3 次,之后不再尝试重启

# ===== 超时设置 =====
TimeoutStartSec=30    # 启动超时时间(默认 90 秒)
TimeoutStopSec=30     # 停止超时时间(默认 90 秒)
TimeoutSec=30         # 同时设置启动和停止超时

# ===== 进程环境 =====
# 工作目录
WorkingDirectory=/opt/myapp

# 运行服务的用户和组(不要用 root!)
User=www-data
Group=www-data

# 环境变量
Environment=PYTHONUNBUFFERED=1
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env.conf
# - 前缀表示文件不存在时不报错

# 标准文件流重定向
StandardInput=null         # stdin(通常不需要)
StandardOutput=journal     # stdout → journald
StandardError=journal      # stderr → journald
# 可选值:null, tty, syslog, journal, kmsg, file:/path

# ===== 资源限制 =====
LimitNOFILE=65535       # 最大文件描述符数
LimitNPROC=1024          # 最大进程数
MemoryMax=512M           # 最大内存限制(需 cgroups v2)
CPUQuota=50%             # CPU 使用上限

[Install] 段 —— 安装与启用

[Install]
# 被哪个 target 依赖(最常用)
WantedBy=multi-user.target

# 创建别名,可以通过此别名管理
Alias=myapp.service

# 同时此服务也"想要"这些服务
Also=myapp-logrotate.timer

常见 Target 说明

Target 对应运行级别 说明
multi-user.target 运行级别 3 多用户命令行模式(最常用
graphical.target 运行级别 5 带图形界面的多用户模式
network-online.target 网络完全就绪后启动
time-sync.target 时间同步完成后启动
basic.target 基础系统初始化完成
shutdown.target 关机时触发

实际场景配置

场景 1:Node.js Web 应用

# /etc/systemd/system/node-app.service
[Unit]
Description=Node.js Web Application
After=network.target

[Service]
Type=simple

# 使用 node 直接启动
ExecStart=/usr/bin/node /opt/node-app/server.js

# 使用 yarn/npm start
# ExecStart=/usr/bin/yarn start
# WorkingDirectory=/opt/node-app

WorkingDirectory=/opt/node-app
User=www-data
Group=www-data

Restart=on-failure
RestartSec=5

Environment=NODE_ENV=production
Environment=PORT=3000

# Node.js 常见优化
LimitNOFILE=65535

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

场景 2:Python 应用(虚拟环境)

# /etc/systemd/system/python-app.service
[Unit]
Description=Python Web Application
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple

# 使用虚拟环境中的 Python
ExecStart=/opt/python-app/venv/bin/python /opt/python-app/app.py

# 或用 gunicorn 启动
# ExecStart=/opt/python-app/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:app

WorkingDirectory=/opt/python-app
User=www-data
Group=www-data

Restart=on-failure
RestartSec=5

Environment=PYTHONUNBUFFERED=1
Environment=DATABASE_URL=postgres:///mydb

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

场景 3:Golang 静态编译二进制

# /etc/systemd/system/go-app.service
[Unit]
Description=Go Application
After=network.target

[Service]
Type=simple

# Go 二进制无需解释器,直接运行
ExecStart=/opt/go-app/server

WorkingDirectory=/opt/go-app
User=www-data
Group=www-data

Restart=on-failure
RestartSec=3

Environment=GIN_MODE=release

# Go 应用通常很节省
LimitNOFILE=65535

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

场景 4:Oneshot —— 开机执行一次脚本

# /etc/systemd/system/my-init-script.service
[Unit]
Description=Run initialization script on boot
After=network.target

[Service]
Type=oneshot

# oneshot 会等待 ExecStart 执行完成
ExecStart=/usr/local/bin/my-init-script.sh

# 即使上次退出失败,下次启动仍执行
RemainAfterExit=no

User=root

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

注意Type=oneshot 用于执行一次性任务(如初始化数据库、清理临时文件)。加上 RemainAfterExit=yes 可以标记为"已执行过",systemctl status 会显示 active (exited) 而非 inactive

场景 5:定时执行(搭配 systemd timer)

Systemd timer 替代 cron 的现代方案:

# /etc/systemd/system/my-hourly-task.service
[Unit]
Description=Hourly maintenance task

[Service]
Type=oneshot
ExecStart=/usr/local/bin/maintenance.sh
User=root

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/my-hourly-task.timer
[Unit]
Description=Run my-hourly-task every hour
Requires=my-hourly-task.service

[Timer]
# 首次开机后 5 分钟执行
OnBootSec=5min

# 之后每小时执行一次
OnUnitActiveSec=1h

# 或者使用日历表达式(每天凌晨 3 点)
# OnCalendar=daily
# OnCalendar=*-*-* 03:00:00

# 即使上次执行失败,下次仍按计划执行
Persistent=true

[Install]
WantedBy=timers.target
# 启动 timer(注意:enable/start timer,不是 service)
sudo systemctl daemon-reload
sudo systemctl enable my-hourly-task.timer
sudo systemctl start my-hourly-task.timer

# 查看所有 timer
systemctl list-timers --all

# 输出示例:
# NEXT                     LEFT          LAST                        PASSED    UNIT                         ACTIVATES
# Fri 2026-05-29 04:00:00 CST 59min left Thu 2026-05-28 03:00:00 CST 22h ago   my-hourly-task.timer         my-hourly-task.service

场景 6:Docker 容器管理服务

# /etc/systemd/system/docker-app.service
[Unit]
Description=My Docker Application Container
# 确保 Docker 已启动
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=simple

# 每次重启都重新创建容器(保证使用最新镜像)
ExecStartPre=-/usr/bin/docker rm -f my-app
ExecStart=/usr/bin/docker run --name my-app \
  -p 8080:8080 \
  -v /opt/myapp/data:/data \
  -e NODE_ENV=production \
  my-app:latest

# 停止时删除容器
ExecStop=/usr/bin/docker stop my-app
ExecStopPost=/usr/bin/docker rm -f my-app

Restart=on-failure
RestartSec=10

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

日志管理(journald)

查看日志

# 基本用法
sudo journalctl -u myapp.service

# 实时跟踪(类似 tail -f)
sudo journalctl -u myapp.service -f

# 查看最近 N 条
sudo journalctl -u myapp.service -n 100

# 查看指定时间范围
sudo journalctl -u myapp.service --since "2026-05-28 00:00:00"
sudo journalctl -u myapp.service --since "1 hour ago"
sudo journalctl -u myapp.service --until "2026-05-28 12:00:00"
sudo journalctl -u myapp.service --since today

# 按日志级别过滤
sudo journalctl -u myapp.service -p err      # 仅错误
sudo journalctl -u myapp.service -p warning  # 警告及以上

# JSON 格式输出(方便程序处理)
sudo journalctl -u myapp.service -o json

# 查看此次启动以来的日志
sudo journalctl -u myapp.service -b

# 查看上次启动的日志(排查崩溃)
sudo journalctl -u myapp.service -b -1

限制日志大小

默认 journald 日志会占用磁盘空间,可以限制其大小:

# 查看当前日志使用量
sudo journalctl --disk-usage

# 配置日志限制
sudo vim /etc/systemd/journald.conf
# /etc/systemd/journald.conf
[Journal]
# 日志最大占用 500M
SystemMaxUse=500M

# 单个日志文件最大 100M
SystemMaxFileSize=100M

# 日志保留 2 周
MaxRetentionSec=2week

# 日志文件达到最大大小后进行压缩
Compress=yes
# 重启 journald 使配置生效
sudo systemctl restart systemd-journald

# 手动清理旧日志
sudo journalctl --vacuum-size=200M    # 保留最近 200M 日志
sudo journalctl --vacuum-time=7d      # 保留最近 7 天日志

排错与调试

服务启动失败

# 1. 查看详细状态(最常用)
sudo systemctl status myapp.service

# 2. 查看最近日志
sudo journalctl -u myapp.service -n 50 --no-pager

# 3. 查看实时日志后重新启动
sudo journalctl -u myapp.service -f &
sudo systemctl restart myapp

# 4. 检查服务文件语法
sudo systemd-analyze verify /etc/systemd/system/myapp.service

# 5. 查看服务文件的实际内容(解析后的最终版本)
sudo systemctl cat myapp.service

常见错误

# 错误 1:服务文件语法错误
# /etc/systemd/system/myapp.service:3: Unknown key 'Descripton'
#                                          ^ 拼写错误!应该是 Description
# 解决:检查拼写,然后运行 systemd-analyze verify

# 错误 2:ExecStart 路径不存在
# Main PID: 12345 (code=exited, status=203/EXEC)
# 解决:确认 ExecStart 中的可执行文件路径正确且可执行

# 错误 3:权限不足
# Main PID: 12345 (code=exited, status=1/FAILURE)
# 解决:检查 User/Group 是否有权访问相关文件和端口
#       (1024 以下端口需要 root 权限)

# 错误 4:端口被占用
# 日志显示:Address already in use
# 解决:检查端口占用,或修改服务端口

# 错误 5:超时
# Timed out starting myapp.service
# 解决:增大 TimeoutStartSec;确认服务启动后不会 hang

# 错误 6:文件被屏蔽
# Unit /etc/systemd/system/myapp.service is masked, ignoring.
# 解决:sudo systemctl unmask myapp.service

调试技巧

# 1. 手动运行 ExecStart 命令(脱离 systemd)
# 这样可以确认程序本身是否能正常运行
sudo -u www-data /usr/bin/python3 /opt/myapp/app.py

# 2. 分析服务启动时间
systemd-analyze blame | head -10

# 3. 查看服务依赖树
systemd-analyze critical-chain myapp.service

# 4. 打印所有服务属性
systemctl show myapp.service

# 5. 临时覆盖服务配置(无需修改文件)
sudo systemctl edit myapp.service
# 这会创建 /etc/systemd/system/myapp.service.d/override.conf

# 6. 模拟启动时环境
sudo systemctl start myapp.service --no-block  # 不阻塞当前终端

安全最佳实践

1. 不要用 root 运行服务

[Service]
# 永远创建专用用户来运行服务
User=myapp-user
Group=myapp-user

# 如果服务不需要写文件,使用 nobody
# User=nobody
# Group=nogroup
# 创建专用系统用户(不能登录、没有 home 目录)
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp-user

2. 使用 systemd 安全隔离特性

[Service]
# 严格限制文件系统访问(只读,仅特定路径可写)
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp /tmp

# 禁止访问 /home、/root、/run/user
ProtectHome=true

# 禁止访问其它进程(自己的子进程除外)
PrivateDevices=true

# 网络隔离
# PrivateNetwork=true    # 如果服务不需要网络

# 禁止内存可执行(禁止某些缓冲区溢出攻击)
MemoryDenyWriteExecute=true

# 限制系统调用(只允许基本调用)
SystemCallArchitectures=native
SystemCallFilter=@system-service

3. 日志安全

[Service]
# 日志不包含敏感环境变量
Environment=MY_SECRET=xxx
# systemd 默认会隐藏环境变量中的密码关键词

4. 文件权限

# 服务文件只允许 root 修改
sudo chmod 644 /etc/systemd/system/myapp.service
sudo chown root:root /etc/systemd/system/myapp.service

# 应用程序文件使用专用用户
sudo chown -R myapp-user:myapp-user /opt/myapp
sudo chmod 750 /opt/myapp

高级技巧

1. 使用 override 目录覆盖配置(推荐)

无需修改原始服务文件,通过 drop-in 配置片段覆盖:

# 会自动创建 /etc/systemd/system/myapp.service.d/override.conf
sudo systemctl edit myapp.service

这会打开编辑器,写入:

[Service]
RestartSec=10
Environment=EXTRA_DEBUG=1

最终效果是这些配置追加到原始配置之上(同名键会覆盖)。查看合并后的配置:

sudo systemctl cat myapp.service

2. 服务间依赖与顺序

# 场景:我的应用需要 PostgreSQL 和 Redis 都就绪后才启动
[Unit]
Description=My App
After=postgresql.service redis.service network-online.target
Requires=postgresql.service redis.service
Wants=network-online.target

# After:  控制启动顺序(先启动依赖)
# Before: 控制启动顺序(本服务先于目标启动)
# Requires: 强依赖,依赖失败则本服务失败
# Wants:    弱依赖,依赖失败不影响本服务
# BindsTo:  强依赖 + 依赖停止时本服务也停止
# PartOf:   依赖停止/重启时本服务也停止/重启

3. 服务模板(实例化服务)

当需要运行同一程序的多个实例时(如每个站点一个 worker):

# /etc/systemd/system/myapp@.service
# 注意文件名中的 @ 符号!
[Unit]
Description=My App Instance (%i)
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/run.sh %i
User=myapp-user
Restart=on-failure

[Install]
WantedBy=multi-user.target
# 启动多个实例
sudo systemctl start myapp@instance1
sudo systemctl start myapp@instance2
sudo systemctl start myapp@instance3

# 设置开机自启
sudo systemctl enable myapp@instance1
sudo systemctl enable myapp@instance2

# 查看状态
sudo systemctl status myapp@instance1

# 批量管理(使用通配符)
sudo systemctl start 'myapp@*'
sudo systemctl status 'myapp@*'

4. 服务崩溃自动通知

[Unit]
Description=My App
OnFailure=myapp-failure-notification.service

# 当此服务进入 failed 状态时,自动启动 myapp-failure-notification.service
# 可用来发邮件、webhook 通知等
# /etc/systemd/system/myapp-failure-notification.service
[Unit]
Description=Notify on My App Failure

[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh %n
# %n = 失败的服务名称

[Install]
WantedBy=multi-user.target

5. 使用 socket 激活(按需启动)

只有当有连接进来时才启动服务:

# /etc/systemd/system/myapp.socket
[Unit]
Description=My App Socket

[Socket]
ListenStream=8080
Accept=false

[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Description=My App Service
Requires=myapp.socket

[Service]
Type=simple
ExecStart=/opt/myapp/app.py
User=myapp-user

[Install]
WantedBy=multi-user.target
# 启用 socket 激活
sudo systemctl enable myapp.socket
sudo systemctl start myapp.socket

# 此时服务尚未启动,但端口已在监听
# 第一个请求到达时,systemd 自动启动服务

6. 查看启动耗时

# 整体启动时间
systemd-analyze

# 每个服务的启动耗时(排序列出)
systemd-analyze blame

# 关键链路的耗时
systemd-analyze critical-chain

# 以 SVG 形式输出启动图
systemd-analyze plot > boot.svg

常见问题

Q:修改了 .service 文件但 systemctl 没生效?

# 每次修改 .service 文件后必须执行
sudo systemctl daemon-reload

# 然后重启服务
sudo systemctl restart myapp

Q:如何让服务在 Docker 启动后再启动?

[Unit]
Description=My App
After=docker.service network-online.target
Requires=docker.service

Q:服务启动了但马上退出(status=exited)

# 可能的原因:
# 1. Type 设置不对(daemon 进程应该用 forking 而非 simple)
# 2. 程序本身有 bug,启动即崩溃
# 3. 依赖的服务未就绪

# 排查方法:
sudo journalctl -u myapp.service -n 50 --no-pager

Q:如何从当前日志中提取 IP 封禁信息?

# 假设你的服务在日志中记录了攻击 IP
sudo journalctl -u myapp.service -o cat | grep "Blocked IP" | grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn

Q:如何让服务在网卡完全就绪后才启动?

[Unit]
After=network-online.target
Wants=network-online.target

同时确保 systemd-networkd-wait-online.serviceNetworkManager-wait-online.service 已启用。

Q:Perl/PHP 脚本怎么配置?

# Perl
[Service]
ExecStart=/usr/bin/perl /opt/myapp/app.pl

# PHP(内置开发服务器,仅用于测试)
[Service]
ExecStart=/usr/bin/php -S 0.0.0.0:8080 -t /opt/myapp/public

Q:Type=simple 和 Type=forking 有什么区别?

  • simple:程序始终在前台运行,systemd 可以跟踪 PID。推荐大多数场景使用。
  • forking:启动后父进程退出,子进程继续在后台运行。传统 daemon 程序(如 nginx、Apache)使用这种方式。需要配合 PIDFile 让 systemd 找到真正的进程。
[Service]
Type=forking
PIDFile=/run/myapp.pid
ExecStart=/opt/myapp/start.sh

Q:环境变量太多怎么办?

# 使用 EnvironmentFile 从文件加载
EnvironmentFile=/etc/myapp/env.conf

# 文件格式(每行一个 KEY=VALUE):
# DATABASE_URL=postgres://localhost/mydb
# REDIS_URL=redis://localhost:6379
# LOG_LEVEL=debug

# 文件路径前的 - 表示文件不存在时不报错
EnvironmentFile=-/etc/myapp/env.conf

Q:服务需要用的端口小于 1024?

Linux 下 1024 以下端口需要 root 权限。两种方案:

方案一:使用 systemd 的 AmbientCapabilities(推荐)

[Service]
User=myapp-user
Group=myapp-user
AmbientCapabilities=CAP_NET_BIND_SERVICE

方案二:使用 authbind

sudo apt install authbind
sudo touch /etc/authbind/byport/80
sudo chown myapp-user:myapp-user /etc/authbind/byport/80
sudo chmod 755 /etc/authbind/byport/80
[Service]
ExecStart=/usr/bin/authbind --deep /opt/myapp/app.py

方案三:用 iptables 转发(生产环境常见)

# 让程序监听 8080,然后转发 80 → 8080
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080

Q:如何让 systemd 在关机时优雅停止我的服务?

[Service]
# systemd 默认发送 SIGTERM,服务应捕获此信号做清理
# 如果服务需要更多时间,设置较长的超时时间
TimeoutStopSec=30

# 发送 SIGTERM 后如果进程未退出,最终发送 SIGKILL
# KillSignal=SIGTERM    # 默认
# SendSIGKILL=yes        # 默认

Q:服务启动成功但端口没监听?

# 可能原因:
# 1. 程序启动后绑定到了 127.0.0.1 而非 0.0.0.0
# 2. 程序崩溃了但 systemd 还没检测到
# 3. AppArmor/SELinux 阻止了端口绑定

# 排查步骤:
sudo ss -tlnp | grep python   # 查看是否有进程在监听
sudo journalctl -u myapp.service -n 30 --no-pager  # 查看是否有错误
sudo aa-status                 # 检查 AppArmor
sudo getenforce                # 检查 SELinux

同行工具对比

特性 Systemd Supervisor PM2 Docker
系统集成度 ⭐⭐⭐⭐⭐ 原生集成 ⭐⭐⭐ 独立进程 ⭐⭐ 仅 Node ⭐⭐⭐ 需额外配置
配置复杂度
自动重启 ✅(需 --restart
日志管理 ✅ journald ✅ 自带日志 ✅ 自带日志 ✅ docker logs
开机自启 ✅ systemctl enable ✅ 需自己配置 ✅ 需自己配置 --restart always
资源限制 ✅ cgroups 原生支持 ✅ cgroups
依赖管理 ✅ 强/弱依赖 ❌(depends_on 仅编排)
学习曲线
适用语言 所有 所有 Node.js 优先 所有(容器化)

选型建议

  • 系统级服务、后台守护进程 → Systemd(不二之选)
  • Python 应用(传统方式)→ SystemdSupervisor
  • Node.js 应用 → SystemdPM2
  • 微服务、需要环境隔离 → Docker + Systemd 管理容器

整体工作流总结

# 1. 创建服务文件
sudo vim /etc/systemd/system/myapp.service

# 2. 重载 systemd
sudo systemctl daemon-reload

# 3. 验证语法
sudo systemd-analyze verify /etc/systemd/system/myapp.service

# 4. 启动服务
sudo systemctl start myapp

# 5. 查看状态(确认正常运行)
sudo systemctl status myapp

# 6. 设置开机自启
sudo systemctl enable myapp

# 7. 查看日志确认无误
sudo journalctl -u myapp.service -f

参考链接