☰
Ubuntu 24.04 上用 systemd 实现 Conda 环境 Python 服务开机自启并等待 Ollama
2026/10/6 3:28:13 网站建设 项目流程

在 Ubuntu 24.04 上,把 conda 虚拟环境里的 Python 程序做成开机自启,还要保证它等 ollama 服务就绪之后才启动——这需求听起来就是写一个 systemd 服务再加几行配置的事。但等你真正动手,就会发现坑全藏在 conda 路径、环境变量和单元文件的启动顺序里。我是在给一台本地模型服务器部署辅助脚本时被这事折腾了一个晚上,今天把整理好的方案写出来,给碰到同样需求的朋友一个可以直接照做的参考。

这套方案的重点不只是“开机启动”,而是“按顺序启动”:系统重启后,ollama 先跑起来,Python 业务脚本后跑。如果顺序反了,脚本里所有调用本地模型接口的逻辑都会连不上服务,轻则报错退出,重则反复重启造成日志刷屏。

1. 为什么多番折腾后选了 systemd 做开机自启

1.1 那些看似可行的老方案,问题出在哪

在 Ubuntu 24.04 上,想让某个程序开机运行,网上能搜到一堆办法:写进/etc/rc.local、在用户目录的.bashrc或.profile里加启动命令、用 crontab 的@reboot触发。这些办法我都试过,遇到复杂点的场景各有各的坑。

  • rc.local 在大多数带 systemd 的发行版里默认就是没有的,你需要手动创建并赋予可执行权限,而且它运行得特别早,网络、用户态服务、ollama 这些大概率还没起来,对于我们这种需要等待 ollama 的场景基本没法用。
  • 把启动命令写到~/.bashrc里是最不推荐的做法。bashrc是交互式 shell 启动时才加载的,如果你用 SSH 登录,每次登录都会触发一次,程序可能被启动多次;就算限制住触发条件,依赖关系也完全不可控。我在一开始图省事试过,后来发现系统一重启,程序根本不会主动出来。
  • crontab 的@reboot是一个可选项,它确实能做开机任务。但 crontab 的调度器管不了服务依赖,你没法在 crontab 里表达“等 ollama 起来再启动我”。如果硬要写,只能在程序里写死 sleep,等待时间短了不够,长了浪费时间,非常笨拙。

真正意义上能把“依赖顺序”“自动重启”“日志管理”“启动状态查询”全做好的,只有 systemd。Ubuntu 24.04 本身就是用 systemd 做 init 系统的,ollama 官方安装脚本也默认帮你注册了 systemd 服务,所以顺着这个思路走最顺。

1.2 systemd 的依赖机制才是解决“先 ollama 后 Python”的正道

systemd 的单元文件里有几个专门控制启动顺序的字段:After=、Before=、Requires=、Wants=。

其中最容易误会的是After=:它只决定当前服务在什么时间点开始启动,但并不会主动去启动对方服务。如果 ollama 没有开机自启,就算你写了After=ollama.service,你的程序启动时 ollama 照样没跑。所以正确姿势是把After=和Wants=配在一起。Wants=是一种“希望它存在并尝试拉起它”的软依赖,即使 ollama 启动失败,也不会强行阻止你的 Python 程序启动。

我最终选用的依赖配置就是下面这两种字段组合在一起:

[Unit] Description=My Conda Python Agent After=ollama.service Wants=ollama.service

这里还有一个容易被忽略的细节:After只表示“ollama 的 unit 已经开始执行”,并不代表“ollama 已经可以对外提供服务”。systemd 服务单元在启动时,进程可能还在初始化,端口未必已经监听。所以即使在 systemd 层面排好了顺序,应用程序这边最好还是做一下就绪检测,后面第 5 章我会专门讲。

2. 动工前先把这三条路径摸清楚

在写任何 service 文件之前,一定要先把环境信息查准确。这一步省了,后面全是灾。

2.1 conda 虚拟环境里 Python 解释器的绝对路径

很多人翻车就翻在这里:明明在终端里激活了 conda 环境,which python指到的路径很干净,结果写进 systemd 服务之后,程序跑起来用的却是别的 Python,导致各种 import 报错。

原因很简单:systemd 服务默认不会加载用户 shell 里的PATH,更不会执行conda activate。所以你必须在服务文件里写死虚拟环境里python解释器的绝对路径。

先查清楚路径:

conda env list

我的输出类似下面这样:

# conda environments: # base * /home/ethan/miniconda3 agent /home/ethan/miniconda3/envs/agent

这个agent环境对应的 Python 绝对路径就是:

/home/ethan/miniconda3/envs/agent/bin/python

也可以用下面这条命令,在激活环境后确认当前解释器路径:

conda activate agent which python

最后你会得到一个类似/home/ethan/miniconda3/envs/agent/bin/python的字符串,把这个路径原封不动地记下来。别用/home/ethan/miniconda3/bin/python,那是 base 环境的解释器,跟你的虚拟环境完全两码事。

2.2 ollama 服务的单元名和执行机制

在 systemd 里,服务的名字就是单元文件名。ollama 官方安装脚本一般会创建一个ollama.service文件,名字就是ollama.service。

在写依赖配置之前,先确认一下:

systemctl list-unit-files | grep ollama

如果输出里有ollama.service,那After=和Wants=直接用这个名字就行。如果查不到,说明 ollama 不是以 systemd 服务方式安装的,可能是你手动下载二进制包用 nohup 跑的。这种情况有两种处理办法:

  • 方法一:给 ollama 补一个 systemd 服务文件,让 ollama 本身也能开机自启。这是最正规的做法,步骤和后面给 Python 程序创建服务完全一样,ExecStart 写 ollama 的绝对路径即可。
  • 方法二:如果 ollama 是手动拉起的,那就不存在“服务启动顺序”这回事了,你只能靠 Python 脚本里的就绪检测去等 ollama 的端口。

实际部署时,我更推荐方法一。原因很简单:ollama 是本地模型服务,开机自启本来就是一个常见需求,给它补一个 systemd 服务后,你还能用systemctl status ollama查看状态、用journalctl -u ollama查日志,后面维护起来轻松很多。

2.3 给程序和日志安排一个清爽的家

写系统服务这件事,最忌讳的就是把脚本、日志、临时文件全都散落在各个目录。我的建议是新建一个专门目录,例如/home/ethan/agent,把 Python 程序放这里,日志也输出到这里。

目录权限要注意:服务以哪个用户身份跑,目录就要对这个用户有读写权限。绝大多数情况下,你的 conda 环境是装在当前用户家目录下的,那服务就应该以这个普通用户运行,而不是用root。用 root 跑 conda 环境里的 Python 经常会踩到 HOME 目录变化、conda 缓存权限、CUDA 设备权限等莫名其妙的问题,我一开始为了省事用过 root,结果各种权限报错,后来老老实实切回普通用户。

可以先去掉当前系统对 pyscript 的支持,也可以先在 shell 里准备好工作目录:

mkdir -p /home/ethan/agent/logs

日志文件路径我习惯用一个固定位置,比如/home/ethan/agent/logs/agent.log,这样后面观察启动状态时,一条命令就能看到所有输出。

3. 手把手配置开机自启的完整过程

准备工作做完,现在进入核心环节。下面每个文件的写法,我都尽量解释清楚为什么这么写。

3.1 先写一个不会迷路的启动脚本

虽然 systemd 的ExecStart可以直接写python /path/server.py,但我强烈建议你再套一层启动脚本。原因有两个:

  • 第一,你可以在脚本里加入等待 ollama 就绪的逻辑,这在第 5 章会展开。
  • 第二,如果以后要加环境变量、加参数,修改脚本比修改 systemd 单元文件方便得多,不用每次都daemon-reload。

新建一个启动脚本/home/ethan/agent/start.sh:

#!/bin/bash exec /home/ethan/miniconda3/envs/agent/bin/python /home/ethan/agent/server.py

注意我用了exec。这个关键字会把当前的 shell 进程替换成 Python 进程,让 Python 程序直接成为 systemd 跟踪的主进程。没有exec的话,shell 会 fork 出一个子进程来跑 Python,systemd 看到的进程是 shell 而不是真正的程序,这样会导致做“优雅停止”和重启策略时很不方便。

给脚本赋予可执行权限:

chmod +x /home/ethan/agent/start.sh

写完先手动跑一遍,确认脚本没问题:

bash /home/ethan/agent/start.sh

如果在终端里能正常跑起来,说明 Python 环境和脚本依赖都没问题。这时候再往 systemd 上搬。

3.2 编写 systemd 服务单元文件(关键一步)

在/etc/systemd/system/下创建一个名为agent.service的文件。为什么放在/etc/systemd/system而不是/lib/systemd/system?因为这个目录是给系统管理员放自定义服务用的,优先级最高,而且不容易被软件包更新覆盖。

文件内容如下:

[Unit] Description=My Conda Python Agent After=ollama.service Wants=ollama.service [Service] Type=simple User=ethan Group=ethan WorkingDirectory=/home/ethan/agent Environment=PATH=/home/ethan/miniconda3/envs/agent/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ExecStart=/home/ethan/agent/start.sh Restart=on-failure RestartSec=5 StandardOutput=append:/home/ethan/agent/logs/agent.log StandardError=append:/home/ethan/agent/logs/error.log [Install] WantedBy=multi-user.target

这里面的每个字段都是我踩过坑后逐个确认的:

Type=simple表示 systemd 认为 ExecStart 启动的进程是主进程,只要进程没有退出,服务就是 active 状态。对于普通的 Python 常驻脚本,simple是最合适的。

User=和Group=指定以哪个用户运行。这里写你的普通用户名,比如ethan。这样条件变量、conda 环境、家目录访问权限都与你的日常工作环境保持一致。

WorkingDirectory=是程序的工作目录。很多 Python 程序会在内部用相对路径读取配置文件或资源文件,如果不设置这个字段,systemd 默认会把根目录/作为工作目录,程序一旦用相对路径立刻报错。

Environment=里把虚拟环境目录加到 PATH 最前面,同时保留系统路径。这样可以保证脚本里如果调用了其他外部命令(比如curl、date),也能找到它们。如果没有这一步,有时候 conda 里的一些子命令会因为找不到可执行文件而报错。

Restart=on-failure表示程序异常退出时自动拉起,这个对常驻后台服务非常关键。RestartSec=5是重启前等待 5 秒,防止程序崩溃后疯狂重启导致 systemd 资源紧张。

StandardOutput和StandardError用append:把日志追加到文件。当然你也可以不写这两行,所有输出默认都会进 journald,用journalctl -u agent查。但我个人更习惯同时留一份文件日志,这样排查问题的时候可以tail -f实时看。

3.3 启用服务并验证开机顺序

写好单元文件后,先让 systemd 重新加载配置:

sudo systemctl daemon-reload

然后启动服务,注意这时它不是开机自启状态,先手动验证一遍:

sudo systemctl start agent

查看服务状态:

sudo systemctl status agent

正常会是active (running)。如果失败,用下面命令看日志:

sudo journalctl -u agent -n 50 --no-pager

手动验证没问题后,设置开机自启:

sudo systemctl enable agent

执行后,系统会在/etc/systemd/system/multi-user.target.wants/下创建一个软链接指向agent.service。这相当于告诉 systemd:进入多用户运行级别的时候拉起这个服务。

到这里,基础的配置已经完成。你可以在命令行里用sudo reboot重启机器,等它起来后直接看:

systemctl status ollama systemctl status agent

如果两个服务都是 active,且 agent 启动时间晚于 ollama,说明基本成功了。

4. 我踩过的坑和排查思路,全在这

配置思路看起来不复杂,但实际操作中,很多问题要等你重启机器后才会暴露。下面几个坑是我自己真实踩过的,按出现频率从高到低整理。

4.1 systemd 环境里 conda 的 PATH 失效

现象:服务启动后,日志里报ModuleNotFoundError: No module named 'requests'。

一开始我以为是依赖没装,结果在终端里激活环境明明能正常 import。问题就出在服务文件里没有正确设置 PATH。systemd 服务启动时,根本不会去加载/home/user/.bashrc里的 conda 初始化代码。

解决方式有两个,推荐组合使用:

  • 第一,ExecStart 里直接写到虚拟环境的 Python 绝对路径,而不是写python。
  • 第二,在[Service]里用Environment=把 PATH 补全。

如果你还需要 conda 环境里的其他可执行文件,也可以在环境变量里加上CONDA_PREFIX:

Environment=CONDA_PREFIX=/home/ethan/miniconda3/envs/agent

4.2 ollama 服务名不存在或没装自启

现象:执行systemctl start agent时提示依赖的 ollama.service 不存在。

如果你systemctl list-unit-files | grep ollama没看到ollama.service,那就是 ollama 并未安装成 systemd 服务。这种情况下,单元文件里的After=ollama.service会变成一个无效引用,虽然不会阻止 agent 启动,但依赖顺序就完全失效了。

我的处理办法是:先给 ollama 补齐 systemd 服务文件。

假设 ollama 二进制在/usr/local/bin/ollama,创建/etc/systemd/system/ollama.service:

[Unit] Description=Ollama Service After=network-online.target [Service] Type=simple User=ethan Group=ethan ExecStart=/usr/local/bin/ollama serve Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

然后systemctl daemon-reload && systemctl enable --now ollama。这样 ollama 本身也能开机自启,agent 的依赖顺序才会真正生效。

4.3 Python 脚本里忘了做就绪检测

现象:agent 服务 active,但日志里疯狂报连接被拒绝,每隔几秒重启一次。

即使 systemd 按顺序启动了 ollama,ollama 从进程拉起到底层模型模块加载完成,还需要几秒甚至十几秒。尤其是你的 Python 程序一启动就去调http://127.0.0.1:11434/api/tags,就很容易撞上 ollama 还没就绪的窗口期。

我当时图省事没写检测,结果系统一重启,agent 就死循环重启。后来学乖了,在启动脚本里加上轮询逻辑,见第 5 章。

4.4 服务里用了 root 运行导致 CUDA 和家目录权限异常

现象:程序能在终端跑,但 systemd 里启动后出现类似could not open shared library或cannot access /home/xxx/.cache的报错。

原因大概率是User=root导致的。用 root 运行后,HOME指向/root,而 conda 环境和模型缓存通常装在普通用户的家目录下。root 没有普通用户家目录的完整路径预期,各种资源找不到。

解决思路很简单,服务文件里显式指定普通用户,并在Environment里设置HOME:

User=ethan Group=ethan Environment=HOME=/home/ethan

这样 conda、Python 缓存目录、模型加载时的路径判断都会回到你熟悉的用户视角。

4.5 修改了服务文件但重启后没生效

如果你改了agent.service里的配置,不改动就重启,很容易出现“改了等于没改”的情况。这不算坑,是 systemd 的机制:它不会每次启动都重新读取磁盘上的单元文件。

正确的操作顺序是:

sudo systemctl daemon-reload sudo systemctl restart agent

如果你改了[Unit]里的依赖配置,最好连 ollama 也一起重启,确保新的依赖关系被整体加载。

5. 让自启更加稳妥的实战细节

前面那套配置已经能解决绝大多数场景,但如果你跟我一样,属于“机器重启后不想天天盯着日志”的人,下面几个优化点一定要跟上。

5.1 用轮询等待 ollama 真正就绪

在启动脚本里加入对 ollama 健康检查的轮询逻辑,重写/home/ethan/agent/start.sh:

#!/bin/bash for i in $(seq 1 30); do if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then break fi sleep 2 done exec /home/ethan/miniconda3/envs/agent/bin/python /home/ethan/agent/server.py

这段逻辑的意思很直白:每 2 秒尝试一次调用 ollama 的接口,如果成功返回就直接 break,最多尝试 30 次,也就是最长等待 60 秒。60 秒后无论 ollama 是否就绪,Python 程序都会照常启动,把后续的容错交给程序内部。

有人可能会问,没有 curl 怎么办?你可以装curl,或者用 Python 自己发 HTTP 请求。考虑到你的 Python 脚本本身肯定装了requests或urllib,用临时方式也是可以的。但我觉得在 shell 脚本里用curl -sf最直接,而且curl本身是个非常通用的工具,一般机器上都有。

这里要特别说明一下curl -sf的好处:-s是安静模式,避免输出进度条;-f是让 HTTP 错误码(比如 404、500)也当作失败处理,而不是让 curl 返回 0。如果不用-f,哪怕接口返回 404 你也会误判为“ollama 已就绪”。

5.2 重启策略、日志轮转和快捷检查

服务文件里已经设置了Restart=on-failure,但如果你希望程序无论任何原因退出都能自动拉起,可以改成Restart=always。对于常驻型 Python 服务,我一般用always,因为一旦它自然退出,说明业务逻辑出问题的概率极大,快速拉起能减少长时间停摆。

日志方面,如果程序运行时间长,建议定期清理logs目录下的文件,或者加简单的日志切割。在 systemd 服务里,我用的是append:追加模式,所以不会有截断问题,但文件会越来越大。可以在 Python 程序里自己用logging.handlers.RotatingFileHandler做按大小切割,这样最省心。

快捷检查启动顺序,可以用下面这条命令:

systemd-analyze critical-chain agent

它会输出 agent 的启动依赖链,显示 ollama 以及更底层网络服务在依赖树里的位置。这比肉眼查时间戳直观得多。它会输出类似:

agent.service └─ollama.service └─network-online.target └─NetworkManager.service

看到这个依赖链,就说明你的After=配置真正生效了。

5.3 重启后的完整验收流程

配置完成,最后一步是完整验收。我的固定流程是这样,你照做一遍,基本能覆盖所有关键点:

sudo reboot

重启后重新登录,按顺序执行:

systemctl status ollama systemctl status agent journalctl -u agent -n 50 --no-pager curl -sf http://127.0.0.1:11434/api/tags

如果前两条都显示 active,日志里没有报错,curl 能正常返回 json 数据,说明整条链路已经打通。

再深一步,可以看具体启动时间确认顺序:

systemctl show agent -p ActiveEnterTimestamp systemctl show ollama -p ActiveEnterTimestamp

通常 agent 的时间应该晚于 ollama。就算时间一样或者早几百毫秒,只要启动脚本里有轮询逻辑兜底,也不用太担心,因为我见过很多情况下 systemd 并发初始化时,时间戳并不能完全反映端口可用性,最终还是要靠业务层检测。

最后再分享一个小技巧:如果你在/home/ethan/agent/logs/agent.log里看到一堆请求失败日志,别急着改 Python 代码,先看看是不是 ollama 本身有没有把模型加载起来。可以用ollama ps查看当前已加载的模型列表。很多时候 ollama 服务活着,模型却不在内存里,Python 脚本请求时会报“model not found”的错误。这种情况处理思路就不在服务启停层面,而是要给 Python 程序加模型预加载逻辑,比如先调用ollama pull或者在启动时请求一次加载接口,把模型预热起来。这个细节容易被忽略,我一开始排查了很久才定位到原因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询