开机启动这件事,说难不难,但如果你没把服务顺序理清楚,多半会踩到"程序启动了但 ollama 还没就绪,跑起来就崩"这种坑。这次我以 Ubuntu 24.04 + conda 虚拟环境 + ollama 为例,把整套开机自启方案完整过一遍,重点就是让 Python 程序在 ollama 服务之后启动。
这篇内容主要解决三件事:conda 虚拟环境里的 Python 怎么能被 systemd 正确调用,开机时怎么保证 ollama 服务先启动,以及中断崩溃之后怎么自动拉起重启。适合刚接触 Ubuntu 服务管理、或者想把自己的本地大模型调用脚本稳定挂在后台的人参考。我会尽量把每一步的"为什么"也讲清楚,少让你在重复试错上花时间。
1. 需求拆解与服务时序思路
1.1 为什么一定要在 ollama 之后启动
很多人的 Python 程序是拿来调用本地 ollama 模型的,比如把文本向量化、跑本地对话接口、自动处理文档摘要等等。这类程序有个共同特点:启动时会对ollama的HTTP接口发起请求,要么是测试连通性,要么直接拉模型做推理。
如果你是手动执行,当然没问题,shell 里敲一下python main.py,等 ollama 跑起来再敲也来得及。但开机自启就不一样了,系统接管了启动顺序,你在终端里养成的"手感"在这里完全失效。
如果 Python 程序先于 ollama 启动,会出现什么?程序尝试请求http://127.0.0.1:11434/api/tags,连接被拒绝,程序抛异常退出,或者进入一个错误的重试死循环。就怕它不退出,而是一直在那里报错空转,把日志刷得老长,等到 ollama 就绪之后它反而"冷静下来"了——这种隐性故障最难查,因为你打开日志一看,全都是启动那几分钟的报错记录,容易误判成别的问题。
所以时序控制的本质是:必须在 ollama 监听端口之后,再拉起来我们的 Python 服务。这个顺序不能靠猜,更不能靠"系统启动管得过来,大概会先起 ollama",必须用 systemd 的依赖关系把它写死。
1.2 方案选型:为什么推荐 systemd 而不是 rc.local 或 crontab
网上也有不少教程推荐用@rebootcrontab,或者往/etc/rc.local里塞启动命令,不能说完全不能用,但在 Ubuntu 24.04 上我强烈建议你别这么做。
rc.local这种传统方式现在默认不在 Ubuntu 24.04 的启动链路里,你需要手动创建服务文件把它"救活",等于自己绕路去解决一个本来就有正规方案的问题。而@reboot的问题是它不提供服务依赖与崩溃自动重启能力,程序挂了就挂了,不会有人帮你拉起来,日志管理也非常原始,stdout 有时候都不好找。
systemd 的 Unit 文件天生就是干这个的:启动顺序用After声明,服务依赖用Requires/Wants表达,崩溃自动拉起用Restart策略,再加上journalctl统一收日志。这就像你雇了一个管家来负责"开灯前先合闸"这种固定流程,而不是每次用胶带把开关贴住。
所以下面所有步骤都围绕 systemd 展开,不折腾旁门左道。
2. 准备工作:conda 虚拟环境与 ollama 服务
2.1 安装并验证 ollama 作为系统服务
先确定一个前提:ollama 在 Ubuntu 24.04 上安装后,默认会自带一个 systemd 服务单元,服务名是ollama.service。这一点非常关键,因为我们后面的"在 ollama 之后启动"要用到这个确切的名称。
安装 ollama 最直接的方式是从 Ollama 官网获取对应系统的安装脚本,装上之后先做三个检查:
# 检查服务状态 systemctl status ollama.service # 查看服务当前是否已经自动起来了 systemctl is-active ollama.service # 确认监听端口 ss -lntp | grep 11434如果状态是active (running),并且11434端口有监听,说明 ollama 作为服务安装成功了。如果你是用普通用户手动ollama serve跑起来的,那开机自启时它不在 systemd 管理范围内,后面的依赖关系就没有对象,需要先把它转成系统服务管理。
有极少数情况下,手动下载二进制解压安装的话不会自动生成 service 文件,那么你需要先补一个最小的 ollama service 单元:
# /etc/systemd/system/ollama.service [Unit] Description=Ollama Local Model Service After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve Restart=always RestartSec=3 User=root [Install] WantedBy=multi-user.target写好后执行systemctl daemon-reload && systemctl enable --now ollama.service。整个方案的前提是 ollama 本身是系统服务,所以这一步务必先落实。
2.2 创建 conda 虚拟环境并安装依赖
接下来准备 Python 侧的环境。假设你的 conda 已经装好,Miniconda 或 Anaconda 都可以。这里注意一个细节:不要直接在你的 base 环境里跑业务程序。因为 base 环境通常承担着 conda 自身的组件维护,搞坏了会影响整个 conda 工具链,而虚拟环境可以随时重建,出了问题不牵连其他项目。
创建环境并安装依赖:
# 创建虚拟环境,这里以 python 3.12 为例 conda create -n myenv python=3.12 -y # 激活后安装项目依赖 conda activate myenv pip install requests # 其他依赖按项目一个个装 # 确认当前环境的python解释器路径 which python我遇到过不少人只在 Windows 上用 conda,到了 Ubuntu 上就很自然地去conda activate,然后执行python main.py,这当然没问题。但我们后面要写 systemd 服务,必须在非交互、非登录 shell 环境下直接指定 Python 解释器。所以你最好现在就把那个路径记下来,例如:
/root/miniconda3/envs/myenv/bin/python如果你没把 conda 装在 root 目录,那就用自己的路径,比如/home/ubuntu/miniconda3/envs/myenv/bin/python。只需要记住一个原则:所有依赖包都要装在这个环境里,后面不再用pip install装到系统级 Python。
有人会想用conda run -n myenv python main.py这种写法来避开路径问题。实测下来conda run在某些场景下会出现输出缓冲异常、退出码不透明的小毛病,而且在 systemd 里多套一层反而增加排查难度,不如直接把绝对路径灌进去干净。
3. 编写 systemd 服务单元文件
3.1 Unit 文件模板与参数讲解
现在我给一个可以直接复制的模板,假设你的项目放在/opt/my-project/,主程序是main.py:
# /etc/systemd/system/myai-worker.service [Unit] Description=My Python AI Worker (conda env) # 等网络和 ollama 都就绪后再启动 After=network-online.target ollama.service Wants=network-online.target Requires=ollama.service [Service] Type=simple User=root WorkingDirectory=/opt/my-project # 使用 conda 虚拟环境里的 python ExecStart=/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py # 如果你的程序有子进程,可能需要加这个 KillMode=control-group # 崩溃之后自动重启 Restart=on-failure RestartSec=5 # 输出缓冲关掉,日志更即时 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target这里几个参数单独解释一下,因为它们是这个方案的核心。
After=ollama.service只保证 systemd 在启动该服务前,先执行 ollama 的启动动作,但它不保证 ollama 已经监听端口。它解决的是"启动编排先后"问题,不解决"进程内部初始化完毕"问题。相当于前面那个管家知道要先合闸,但要等多久、灯到底亮了没有,那得另说。
Requires=ollama.service和After的区别比较微妙。Requires表示强依赖:如果 ollama.service 启动失败,那么我这个服务就不应该启动。如果你只是"希望" ollama 先跑,但即使 ollama 失败也不影响 Python 程序起来,那用Wants=ollama.service更合适。我这里写Requires,因为我们的业务逻辑明确依赖 ollama 接口,ollama 起不来那程序起来也是白起。
Restart=on-failure加上RestartSec=5,这是生产环境服务的基本生存保障。程序因为一次网络抖动、断连、异常崩溃退出了,systemd 会在 5 秒后自动重新拉起,不需要人肉盯着。
3.2 关键点:ExecStart 必须用 conda 虚拟环境的绝对路径
很多人在这里翻车。你在终端里执行conda activate myenv能成功,不代表 systemd 启动的服务也能直接用python命令。
原因在于 conda 的 activate 其实是一个 shell 函数,它会修改当前 shell 的 PATH 环境变量,把envs/myenv/bin插到最前面。而 systemd 启动服务的时候,环境是很干净的,继承的 PATH 可能根本没有 conda 的目录,所以你在 ExecStart 里写ExecStart=python main.py,大概率会变成"找不到 python,服务启动失败"。
解决方案就是上面模板里的绝对路径写法:
ExecStart=/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py这样不依赖 PATH,不需要 activate,解释器直接锁定版本和依赖环境。就好比你去后厨点名要某个厨师掌勺,而不是说"来个人做饭",谁是主厨无所谓,但你要的是那个特定环境里的 Python 和它绑定的包。
另外,如果你的项目还需要读取环境变量文件,可以在 Unit 文件里加EnvironmentFile=/opt/my-project/.env。这里要注意的是,如果想要暴露变量给 ExecStart 的进程,尽量用Environment=逐条写,或者用EnvironmentFile=引入整个文件,比你预想用 shell 脚本source要可靠得多。
3.3 ExecStartPre 等待 ollama 端口真正就绪
前面说After只管启动顺序,不管端口就绪。很多老手为了让服务更"皮实",会在 ExecStart 之前加一段端口探测脚本,叫做ExecStartPre。
逻辑就是:循环检查 ollama 的接口是否返回正常,最多等 60 秒,如果期间成功就往下走,如果超时就让服务失败,避免程序启动后立刻开摆。
[Service] ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -s -o /dev/null http://127.0.0.1:11434/api/tags && exit 0; sleep 2; done; exit 1' ExecStart=/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py这条脚本是你自己程序与 ollama 之间的"握手环节"。curl探测到接口通畅后,才认定 ollama 已经就绪。30 次循环、每次 sleep 2 秒,也就是最多等待 60 秒。ollama 在冷启动加载模型的时候是可能有几秒延迟的,这个窗口够用了。
注意:如果你的系统里没有 curl,先用apt install curl -y装上,或者改成用/root/miniconda3/envs/myenv/bin/python -c "import urllib.request..."来做探测,但那样绕远路,没必要。
4. 实操部署步骤
4.1 放置 Unit 文件并加载配置
首先把上面写好的 Unit 内容保存到/etc/systemd/system/myai-worker.service。如果你用普通用户操作,记得加 sudo。文件命名建议跟服务用途匹配,比如myai-worker.service,这个文件名就是日后你管理服务的名字。
写入之后,先别急着启动,让 systemd 重新加载配置文件:
sudo systemctl daemon-reload这一步必胜不可漏掉,否则 systemd 还在用内存里的旧配置,你怎么操作它都觉得奇怪。
4.2 启动服务并设置开机自启
先手动启动一次,而不是立刻去开 enable:
sudo systemctl start myai-worker.service sudo systemctl status myai-worker.service如果状态显示active (running),说明基本OK了。接下来设置开机自启:
sudo systemctl enable myai-worker.serviceenable的本质是在/etc/systemd/system/multi-user.target.wants/下创建一个软链接,让系统进入多用户目标时自动拉起该服务。你不需要理解这个软链接机制就能用,但知道这点有助于排查"为什么已经设置了 enable 还没起来"的疑惑。
这时可以看一下依赖状态:
systemctl list-dependencies myai-worker.service systemctl status ollama.service确认两个服务都在运行,再往下测试。
4.3 验证与测试:重启后看效果
这一步不要跳过。即使你现在看到服务在跑,也必须模拟一次开机过程,否则所有依赖关系只是在"顺风局"里有效。最省事但最真实的方式是重启机器:
sudo reboot重启后回到系统,立刻依次执行:
systemctl is-active ollama.service systemctl is-active myai-worker.service journalctl -u myai-worker.service -n 50 --no-pager理想情况是:ollama 处于 active,你的服务也处于 active,日志里能看到 Python 程序按预期开始了初始化,而且能够访问 ollama 接口。
如果不想重启物理机,也可以用systemctl restart myai-worker.service来测试服务的自我恢复能力,但开机时序的真实效果还是建议重启一次。
5. 常见问题与排查记录
5.1 conda activate 在 systemd 里用不了怎么办
这个问题几乎是本方案的第一大坑。现象是:Unit 文件里写了ExecStart=conda run -n myenv python main.py,服务启动后journalctl显示找不到 conda 命令,或者明明 activate 成功了却 import 不到包。
原因是 systemd 启动的服务默认不会加载用户的 shell 配置文件,conda这个命令不在 PATH 里。你可以在 Unit 里写:
Environment=PATH=/root/miniconda3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin但更稳妥的做法依然是用虚拟环境内 python 的绝对路径来启动,完全绕开 activate 这一步。我的习惯是:凡是 systemd 里的任务,只用绝对路径,不搞任何 shell 函数依赖。
如果你实在要保留 conda 的激活过程,可以套一层 bash 登录壳:
ExecStart=/bin/bash -lc 'source /root/miniconda3/etc/profile.d/conda.sh && conda activate myenv && python /opt/my-project/main.py'但这个写法也容易踩坑,比如bash -lc可能会引入额外的 PATH 覆盖、加载一堆用户 profile,导致你在终端完美运行的程序到 systemd 里行为不一致。不如前面模板里的绝对路径干净。
5.2 服务启动失败与重启策略排查
如果你的服务启动就退了,第一步不是改代码,而是看两个东西:状态码和日志。
systemctl status myai-worker.service -l journalctl -u myai-worker.service -f常见的失败信号包括:
| 现象 | 大概率原因 | 解决方向 |
|---|---|---|
状态显示ExecStart路径找不到 | python 路径不对 | 用which python在 conda 环境内确认绝对路径 |
日志出现ModuleNotFoundError | 依赖没装在虚拟环境里 | 检查pip list和 python 路径是否对应同一环境 |
日志出现Connection refused | ollama 还没起来,或等待时间不够 | 加长ExecStartPre探测循环,确认 ollama 监听 |
服务一直activating不进入 running | 脚本卡住 | 检查ExecStartPre是否死循环,建议探测脚本只做有限次重试 |
关于Restart策略,我给个推荐值:业务比较重要的用Restart=always,但注意如果代码在启动阶段因为配置错误反复崩溃,always可能会导致你改配置时服务一直在重启、难以稳定操作。更合理的是Restart=on-failure,区别是当进程被管理员手动停止时,always会把它拉起来,但on-failure不会。
我自己的项目习惯是:开发测试阶段用Restart=no,看崩溃日志看完再说;基本稳定后改成Restart=on-failure,RestartSec=5。
5.3 日志与权限排查
日志是排查服务问题的第一现场。systemd 会把服务的 stdout 和 stderr 全部收进 journal,直接看:
journalctl -u myai-worker.service -n 100 journalctl -u myai-worker.service --since "5 minutes ago"如果你的程序通过 Python 的print输出,但在日志里看不到任何内容,检查 Unit 里有没有设置Environment=PYTHONUNBUFFERED=1。Python 在非终端环境里输出是块缓冲的,程序不退出可能就一直攒在内存里,日志割裂感很强。
权限问题也是一大类。比如你的主程序需要访问模型文件、需要写日志文件到/var/log/myapp/,但 User=root 没权限写入某个受保护的目录,或者反过来你用普通用户 User=ubuntu,但访问 /opt/my-project 下的文件时没有读权限。解决方向很明确:要么给目录设置合适的属主,要么统一让服务和目录都归同一个用户管理。别写出一个逻辑上没问题但权限上"怎么都不对"的配置出来。
6. 补充:我踩过的几个细节坑
第一个是 WorkingDirectory。有些人觉得 ExecStart 里已经写了绝对路径,WorkingDirectory 就可有可无,但很多 Python 程序会隐式依赖当前工作目录去读相对路径的配置文件,比如./model_config.json。你在终端里跑的时候,当前目录是项目根目录,没问题;但 systemd 启动时,默认工作目录是/,相对路径全失效,程序会报 FileNotFoundError。所以模板里一定要写WorkingDirectory=/opt/my-project,除非你代码里全部用了绝对路径。
第二个是环境变量的统一。我试过在.bashrc里export一堆变量,结果 systemd 完全看不到。最好的做法是把所有业务需要的环境变量集中在/etc/myai-worker.env文件里,然后在 Unit 中指定:
EnvironmentFile=/etc/myai-worker.env注意这个文件的格式是KEY=VALUE,不需要export前缀,也不能有 shell 语法。我见过有人往里面写export导致 systemd 直接拒绝加载全部变量,这种问题很隐蔽。
第三个是关于服务间依赖"最后一公里"的问题。即便我写了After=ollama.service,也无法保证 ollama 一定能完成模型加载或 API 服务稳定。所以我在生产环境里真正交付用的 Unit,一定会带上端口检测的ExecStartPre,等真正确认api/tags返回 200 之后再拉 Python 程序。这不是防御性编程,而是把机器启动的不确定性当成常态来处理。
整个流程如果已经走通,你可以用一个测试脚本再验证一遍:故意停掉 ollama,然后重启你的服务,看看它是不是因为Requires依赖而拒绝启动;再把 ollama 拉起来,手动启动你的服务,看它是否能自动恢复。把这种"故障演练"做一遍之后,你对这套开机自启组合的理解会比只看文档深刻得多。