
在 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 没有开机自启就算你写了Afterollama.service你的程序启动时 ollama 照样没跑。所以正确姿势是把After和Wants配在一起。Wants是一种“希望它存在并尝试拉起它”的软依赖即使 ollama 启动失败也不会强行阻止你的 Python 程序启动。我最终选用的依赖配置就是下面这两种字段组合在一起[Unit] DescriptionMy Conda Python Agent Afterollama.service Wantsollama.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 出一个子进程来跑 Pythonsystemd 看到的进程是 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] DescriptionMy Conda Python Agent Afterollama.service Wantsollama.service [Service] Typesimple Userethan Groupethan WorkingDirectory/home/ethan/agent EnvironmentPATH/home/ethan/miniconda3/envs/agent/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ExecStart/home/ethan/agent/start.sh Restarton-failure RestartSec5 StandardOutputappend:/home/ethan/agent/logs/agent.log StandardErrorappend:/home/ethan/agent/logs/error.log [Install] WantedBymulti-user.target这里面的每个字段都是我踩过坑后逐个确认的Typesimple表示 systemd 认为 ExecStart 启动的进程是主进程只要进程没有退出服务就是 active 状态。对于普通的 Python 常驻脚本simple是最合适的。User和Group指定以哪个用户运行。这里写你的普通用户名比如ethan。这样条件变量、conda 环境、家目录访问权限都与你的日常工作环境保持一致。WorkingDirectory是程序的工作目录。很多 Python 程序会在内部用相对路径读取配置文件或资源文件如果不设置这个字段systemd 默认会把根目录/作为工作目录程序一旦用相对路径立刻报错。Environment里把虚拟环境目录加到 PATH 最前面同时保留系统路径。这样可以保证脚本里如果调用了其他外部命令比如curl、date也能找到它们。如果没有这一步有时候 conda 里的一些子命令会因为找不到可执行文件而报错。Restarton-failure表示程序异常退出时自动拉起这个对常驻后台服务非常关键。RestartSec5是重启前等待 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_PREFIXEnvironmentCONDA_PREFIX/home/ethan/miniconda3/envs/agent4.2 ollama 服务名不存在或没装自启现象执行systemctl start agent时提示依赖的 ollama.service 不存在。如果你systemctl list-unit-files | grep ollama没看到ollama.service那就是 ollama 并未安装成 systemd 服务。这种情况下单元文件里的Afterollama.service会变成一个无效引用虽然不会阻止 agent 启动但依赖顺序就完全失效了。我的处理办法是先给 ollama 补齐 systemd 服务文件。假设 ollama 二进制在/usr/local/bin/ollama创建/etc/systemd/system/ollama.service[Unit] DescriptionOllama Service Afternetwork-online.target [Service] Typesimple Userethan Groupethan ExecStart/usr/local/bin/ollama serve Restarton-failure RestartSec5 [Install] WantedBymulti-user.target然后systemctl daemon-reload systemctl enable --now ollama。这样 ollama 本身也能开机自启agent 的依赖顺序才会真正生效。4.3 Python 脚本里忘了做就绪检测现象agent 服务 active但日志里疯狂报连接被拒绝每隔几秒重启一次。即使 systemd 按顺序启动了 ollamaollama 从进程拉起到底层模型模块加载完成还需要几秒甚至十几秒。尤其是你的 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的报错。原因大概率是Userroot导致的。用 root 运行后HOME指向/root而 conda 环境和模型缓存通常装在普通用户的家目录下。root 没有普通用户家目录的完整路径预期各种资源找不到。解决思路很简单服务文件里显式指定普通用户并在Environment里设置HOMEUserethan Groupethan EnvironmentHOME/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 21; 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 重启策略、日志轮转和快捷检查服务文件里已经设置了Restarton-failure但如果你希望程序无论任何原因退出都能自动拉起可以改成Restartalways。对于常驻型 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或者在启动时请求一次加载接口把模型预热起来。这个细节容易被忽略我一开始排查了很久才定位到原因。