Ubuntu 创建系统服务与开机自启完全指南
概述
在 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.service 或 NetworkManager-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 应用(传统方式)→ Systemd 或 Supervisor
- Node.js 应用 → Systemd 或 PM2
- 微服务、需要环境隔离 → 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