在Ubuntu服务器上部署自己的程序时,很多人习惯用nohup或者crontab的@reboot来让程序在后台运行。这些方式虽然简单,但缺点也很明显:程序挂掉后不会自动重启,开机启动的时机不可控,日志散落各处难以查询。systemd作为Ubuntu默认的初始化系统,提供了完善的守护进程管理能力,只需编写一个配置文件,就能让程序获得自动启动、故障重启、日志收集等企业级特性。本文将从零开始,手把手演示如何在Ubuntu上创建一个自定义的systemd服务。

一、编写systemd服务配置文件
systemd管理的每一个服务都对应一个unit文件,自定义服务一般存放在/etc/systemd/system/目录下,文件名以.service结尾。系统自带的服务则放在/lib/systemd/system/中,两个目录不要混用,自定义服务统一放前者便于管理。
下面以运行一个Python脚本为例,创建文件/etc/systemd/system/myapp.service,写入如下内容:
[Unit] Description=My Custom Application Service After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 /opt/myapp/main.py Restart=on-failure RestartSec=5 Environment=APP_ENV=production Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target
配置分为三个段。[Unit]段描述服务的基本信息,Description是服务的说明文字,After=network.target表示在网络就绪之后再启动,如果程序依赖数据库或网络,这一行必不可少,否则开机时可能因为网络未就绪而启动失败。
[Service]段是核心。Type=simple表示主进程就是前台运行的那个进程,这是最常用的类型;如果程序会自动fork到后台,则需要改用Type=forking。ExecStart指定启动命令,注意必须写绝对路径,systemd不会加载用户的PATH环境变量,直接写python3会报找不到命令的错误。Restart=on-failure让程序异常退出后5秒自动重启,Environment用来注入环境变量,避免把敏感信息硬编码到代码里。
[Install]段的WantedBy=multi-user.target定义了服务的安装位置,multi-user.target对应常见的多用户命令行运行级别,绝大多数服务都用这个值。如果希望服务随图形界面启动,可以改成graphical.target,但一般没有必要。
二、启用服务并设置开机自启动
配置文件写好后,需要先让systemd重新加载所有unit文件,然后再启动服务并设置为开机自启。完整的命令流程如下:
# 重新加载systemd配置,每次修改service文件后都要执行 sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapp.service # 设置开机自启动(建立软链接到multi-user.target.wants目录) sudo systemctl enable myapp.service # 查看服务当前状态 sudo systemctl status myapp.service
这里需要特别说明start和enable的区别,很多初学者容易混淆。start是立即启动服务,只影响当前这一次;enable是设置开机自动启动,它并不会启动服务,只是在/etc/systemd/system/multi-user.target.wants/目录下创建一个指向unit文件的软链接。想让服务立刻运行并且重启后也自动启动,两条命令都要执行。也可以用sudo systemctl enable --now myapp.service一步到位。
对应的关闭操作也有两组:stop停止服务、disable取消开机自启。如果修改了service文件内容,除了daemon-reload之外,还需要restart才能让新配置生效。日常排查时可以用systemctl is-active myapp快速判断服务是否在运行,返回active说明正常,返回failed则需要进一步看日志。
三、日志查看与常见问题排查
systemd统一接管了服务的标准输出和标准错误,查看日志不再需要翻找nohup.out文件,直接使用journalctl命令即可:
# 查看服务的全部日志 sudo journalctl -u myapp.service # 实时追踪日志输出,类似tail -f的效果 sudo journalctl -u myapp.service -f # 只看最近100行日志 sudo journalctl -u myapp.service -n 100 # 查看今天的日志 sudo journalctl -u myapp.service --since today
服务启动失败时,status命令的输出通常会给出关键提示,比如exit-code和status后面的数字对应程序退出码。常见的几类错误包括:第一,ExecStart路径写错或者使用了相对路径,报错一般显示no such file or directory;第二,User指定的用户对WorkingDirectory没有访问权限,导致启动即退出,可以检查目录属主或者临时把User改成root测试;第三,Python脚本缺少虚拟环境依赖,因为systemd不会自动激活venv,解决办法是在ExecStart中直接写虚拟环境中的解释器绝对路径,例如/opt/myapp/venv/bin/python3。
还有一种情况是服务状态显示active但程序实际没有工作,这多半是Type配置和程序行为不匹配造成的。比如程序启动后立即fork子进程然后父进程退出,Type=simple下systemd会跟踪错误的进程。此时可以改用Type=forking并配合PIDFile指定pid文件,或者干脆修改程序让它保持前台运行,后者是更推荐的做法,systemd体系下前台化才是主流设计。
另外提醒一点,如果服务以普通用户身份运行,日志默认持久化在/var/log/journal/,Ubuntu默认已经开启。磁盘空间紧张时可以通过sudo journalctl --vacuum-size=100M清理旧日志,控制日志占用的空间。
四、进阶配置技巧
掌握了基本用法后,还可以通过一些进阶配置让服务更加健壮。比如限制服务占用的资源,防止程序失控拖垮整机:
[Service] # 限制内存使用上限为512M,超出后被系统终止 MemoryMax=512M # 限制CPU使用率为单核的50% CPUQuota=50% # 限制能打开的文件描述符数量 LimitNOFILE=65535 # 无论何种退出原因都重启,包括正常退出 Restart=always # 禁止服务获得额外权限 NoNewPrivileges=true # 使用私有临时目录,避免与其他服务互相干扰 PrivateTmp=true
服务之间还可以建立依赖关系。假设myapp依赖Redis,可以在Unit段写Requires=redis-server.service和After=redis-server.service,这样启动myapp时systemd会先把Redis拉起来。反向的场景则可以用BindsTo,当Redis停止时myapp也跟着停止,避免程序在依赖缺失的状态下空转。
对于需要执行一次性任务的场景,比如开机初始化脚本,可以定义Type=oneshot并加上RemainAfterExit=yes,这样脚本执行完服务状态仍显示为active,便于后续判断初始化是否已完成。如果是定时任务,则应该使用timer类型的unit配合OnCalendar字段,比crontab更灵活且日志更完整,这里不再展开,有兴趣的话可以参考systemd.timer的官方文档。
总结
创建自定义systemd服务的关键步骤可以归纳为四步:在/etc/systemd/system/下编写service文件、执行daemon-reload、start启动服务、enable设置开机自启。核心配置集中在Service段,注意命令写绝对路径、合理选择Type、配置Restart实现故障自愈。遇到问题时优先用status和journalctl定位原因,绝大多数启动失败都能从这两处输出中找到答案。把后台程序交给systemd管理后,服务器的稳定性和可维护性会有明显提升,这也是生产环境的标准做法。