Ubuntu 24.04用Docker Compose部署Whisper语音识别服务全指南
如果你需要在Ubuntu 24.04上搭一个完全本地的语音识别服务把录音、播客、会议音频批量转成文本同时又不想被一堆Python依赖折腾到怀疑人生那Whisper配合Docker Compose几乎是我目前试过最省心的组合。我最近把整套流程从零跑通从模型选型到容器编排、GPU透传、接口调用都踩了不少坑这篇文章就是完整记录适合想在内网、实验室或者自己家搭建离线语音转写服务的人参考也适合刚接触Docker和语音模型的初学者直接抄作业。1. 为什么选Docker Compose来部署Whisper而不是直接裸装1.1 环境隔离与依赖冲突是最大的理由Whisper这个模型本身很轻真正折磨人的是它背后的运行环境。模型推理依赖的深度学习框架、音频解码组件、分词器、数值计算库每一个都可能和系统里的其他软件打架。Ubuntu 24.04默认自带的Python版本较新很多编译型依赖包在安装时会对GLIBC版本、GCC版本、CUDA工具链有要求。直接pip安装的话今天为了修某个音频库的编译错误动一次系统Python明天又为了某个工具的兼容性改一遍环境变量最后系统怎么坏的都不知道。用Docker Compose之后整个运行环境被完整封装进镜像Python版本、ffmpeg版本、依赖库、启动命令全都在镜像里固化下来。宿主机只要装了Docker和对应的GPU容器运行时就能直接跑不会污染系统环境。换一台机器部署时只需要拷贝一份项目目录一条docker compose up就能拉起同样的服务不用再逐个确认每个依赖的版本。1.2 Compose解决了“部署一次到处复现”的问题用Docker单命令也能启动容器但如果涉及多个服务、多个挂载目录、环境变量、重启策略、健康检查命令会越来越长。Compose把这些配置收敛到一个YAML文件里用声明式的方式描述整个服务的运行状态。我这边项目是同时要挂载模型目录、音频输入目录、输出目录还要限制容器资源、配置健康检查。这些全写在compose里比写一堆docker run参数清晰得多。而且因为是文本文件可以直接进版本库同事拿到项目目录就能复现不用靠聊天记录里的历史命令。另外一个容易被忽略的收益是升级和回滚。镜像重新构建或者改用不同版本时compose文件里只改几行重新up一次就能完成状态切换。相比手工维护一套Python虚拟环境可操作性高一个量级。我实际测下来整个容器化流程只要网络正常从目录创建到服务响应大概是几分钟的事。2. 部署前必须想清楚的三个关键点2.1 模型尺寸选型决定你能转出什么质量Whisper官方提供了多个尺寸的模型这是部署前第一个要决策的点。很多人一上来就直接上大模型结果资源吃紧、速度慢、调度问题频发最后反而觉得项目难搞。模型尺寸、资源占用和识别质量之间的关系非常直接没有免费午餐。模型尺寸参数规模内存占用参考转写速度适用场景tiny约39M1GB以内极快实时性要求高、对精度要求低的场景base约74M1~2GB快简单命令词、短句识别small约244M2~4GB中等日常语音转写、中文普通话medium约769M5~10GB较慢对质量有要求、资源充足的机器large-v3约1550M10GB以上慢追求最高识别质量、不赶时间以中文场景为例tiny和base在带口音、嘈杂环境下基本不可用措辞容易被识别成奇怪的内容。small开始能应付大多数日常音频medium在长音频、多说话人场景下明显更稳large-v3则适合对字幕质量有硬性要求的场景。如果只是自己整理会议录音推荐small到medium之间起步如果是商业质检、字幕组批量生成直接考虑large-v3配GPU。这里有一个我强烈建议的做法先用small把整个链路跑通确认服务正常、接口稳定之后再切换更大模型。直接上large如果遇到OOM或者加载超时你都不知道问题出在环境还是模型排查成本会高很多。2.2 CPU、GPU与量化方式动手前先定方案同一个模型在不同硬件上的表现差距非常大。先说结论没有独立显卡的时候CPU模式加int8量化是最稳妥的组合有N卡且显存足够的时候用CUDA加float16是明显更优的选择。CPU部署有一个好处兼容性极好几乎任何机器都能跑。但小尺寸模型在CPU上转写一段几分钟的音频可能需要数倍于音频时长的时间。int8量化能把内存占用和推理速度都优化不少识别质量损失在small以下几乎可以忽略在medium级别会有一点点可感知的差异但通常不影响阅读。GPU部署则需要先确认CUDA环境能被容器访问否则即使宿主机跑着显卡驱动容器里也检测不到。这一块的问题我在后面“常见问题”里专门讲。硬件方案一旦确定compose文件、代码里的设备参数、量化类型都要保持一致尤其是compute_type设错轻则速度下降重则直接加载失败。2.3 镜像选择与依赖清单别贪图“一行命令跑起来”网上有一些现成的Whisper服务镜像拉下来就能用确实方便。但现成镜像的问题在于你不知道里面编译了什么、版本是否冻结、有没有不必要的组件。镜像体积一大内网部署拉取本身就耗时。我更推荐基于一个干净的Python基础镜像自己写几行Dockerfile把ffmpeg、推理库、Web框架装好。这样镜像虽然也是自己构建的但每一层都能看懂后期升级、排查都更可控。依赖清单里最容易出问题的两个一个是ffmpegWhisper依赖它做音频解码系统里没有或者版本太老会导致转写结果为空另一个是Hugging Face相关的下载组件它负责模型缓存和加载。在容器构建阶段就把这些装好比在运行时才发现缺依赖要省心得多。3. 从零开始的完整部署过程3.1 前置准备装Docker、建立目录结构、准备模型文件先确认Docker环境。Ubuntu 24.04可以直接用发行版自带的包也可以从Docker官方仓库装两者区别不大。我自己用的是发行版自带的sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER安装结束后重新登录一次让用户组生效。验证一下docker info docker compose version不需要sudo就能执行docker命令说明当前用户已经在docker组里生效了。然后设计目录结构。我这边项目用的是这样的布局/opt/whisper-local/ ├── compose.yaml ├── Dockerfile ├── requirements.txt ├── app.py ├── models/ │ └── faster-whisper-small/ ├── audio/ └── output/audio目录放待转写的录音output目录放转写结果models目录放模型文件。最关键的是模型文件不要等到容器启动时再现场从网络下载而是提前下载到models目录里容器启动时直接挂载进去。这样既避免了运行时下载慢、超时的问题也让整个服务完全离线可用。下载模型我用的是模型仓库的CLI工具命令大致如下pip install -U huggingface_hub huggingface-cli download 模型ID --local-dir ./models/faster-whisper-small模型ID占位部分根据你选择的模型尺寸填写。下载完成后确认models/faster-whisper-small目录下包含config.json、model.bin、tokenizer.json等完整文件再启动服务。这一步值得多花几分钟检查我遇到过模型文件下了一半就强行启动的情况容器日志里一直是加载失败的报错。3.2 写一个能直接用的compose.yaml在项目根目录创建compose.yaml这是整个服务的核心描述文件。以下是我实际在用的版本参数都标注了用途name: whisper-local services: whisper-server: build: . image: whisper-server:local container_name: whisper-local restart: unless-stopped ports: - 8000:8000 volumes: - ./models:/models:ro - ./audio:/audio - ./output:/output - whisper-cache:/root/.cache environment: - WHISPER_MODEL_DIR/models/faster-whisper-small - DEVICEcpu - COMPUTE_TYPEint8 - WHISPER_LANGUAGEzh mem_limit: 6g cpus: 4.0 healthcheck: test: [CMD-SHELL, curl -sf http://localhost:8000/health || exit 1] interval: 30s timeout: 10s retries: 3 start_period: 120s volumes: whisper-cache:解释几个关键配置restart: unless-stopped系统重启、Docker重启后服务自动拉起适合无人值守的转录服务。ports: 8000:8000宿主机8000端口映射到容器内8000端口后续局域网内其他机器也能通过这台机器的IP访问。whisper-cache命名卷避免镜像重建时模型重复缓存下载。虽然我们手动挂载了模型目录但推理库可能还会在/root/.cache下写一些临时缓存保留这个卷能加速二次启动。mem_limit和cpus给容器设资源上限防止一次并发转写把整台机器拖垮。数值根据你实际机器内存调整如果部署large模型建议mem_limit至少16g。start_period: 120s模型加载需要时间给健康检查一个宽限期不然刚启动就被判定为不健康还会反复重启。3.3 准备镜像与后端代码在项目根目录创建DockerfileFROM python:3.11-slim RUN apt-get update apt-get install -y ffmpeg curl rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD [uvicorn, app:app, --host, 0.0.0.0, --port, 8000]requirements.txt内容如下faster-whisper fastapi uvicorn python-multipart这里之所以用faster-whisper而不是原始的Whisper是因为它基于CTranslate2重新实现了推理部分在CPU和GPU上的速度都有明显提升内存占用也更低。API返回的转写结果我们只需要拼接即可代码里可以保持简单。后端接口文件app.py我写了一个精简但干净可用的版本import os import tempfile from fastapi import FastAPI, File, UploadFile from faster_whisper import WhisperModel MODEL_DIR os.getenv(WHISPER_MODEL_DIR, /models/faster-whisper-small) DEVICE os.getenv(DEVICE, cpu) COMPUTE_TYPE os.getenv(COMPUTE_TYPE, int8) LANGUAGE os.getenv(WHISPER_LANGUAGE, zh) app FastAPI() model WhisperModel(MODEL_DIR, deviceDEVICE, compute_typeCOMPUTE_TYPE) app.get(/health) async def health(): return {status: ok, model: MODEL_DIR} app.post(/transcribe) async def transcribe(file: UploadFile File(...), language: str LANGUAGE): suffix os.path.splitext(file.filename or audio.wav)[1] or .wav with tempfile.NamedTemporaryFile(suffixsuffix, deleteFalse) as tmp: tmp.write(await file.read()) tmp_path tmp.name segments, info model.transcribe( tmp_path, languagelanguage, beam_size2, vad_filterTrue, ) text .join(seg.text for seg in segments) os.unlink(tmp_path) return { text: text, language: info.language, duration: round(info.duration, 2), }这里要注意一个问题model.transcribe返回的是一个生成器遍历segments时模型才真正执行推理。所以不要在前面打印或测量生成器而是在遍历完后再取info.duration否则可能出现时长信息和文本不匹配的诡异现象。我一开始在这个地方踩过坑以为模型没跑完其实是生成器还没被消耗。3.4 启动容器、测试转写接口、验证服务健康目录结构准备好后在项目根目录执行docker compose up -d --build第一次构建会比较慢因为要拉基础镜像、装依赖。构建完成后查看容器状态docker compose ps docker compose logs -f看到Started server process或者健康检查通过后先用curl验证服务存活curl -s http://127.0.0.1:8000/health返回{status:ok,model:/models/faster-whisper-small}说明容器正常。然后准备一段测试音频。注意Whisper对音频格式的要求其实并不挑剔但为了减少前端出错的概率我习惯先把待转写文件转成16kHz采样率、单声道的WAVffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le test.wav然后调用转写接口curl -X POST http://127.0.0.1:8000/transcribe \ -F file./test.wav \ -F languagezh返回结果类似{text:今天下午的会议讨论了三件事第一是容器化部署的进度……,language:zh,duration:15.36}能拿到文本就说明整套链路通了。如果返回空文本大概率是音频解码问题优先检查容器内ffmpeg是否正常工作docker exec whisper-local ffmpeg -version3.5 精度与并发参数调优服务跑通之后面临的真实问题是转写结果能不能更准、并发请求会不会把机器打爆。我的调优顺序是这样的先固定语言再调beam size最后决定是否启用VAD过滤。固定语言对速度的提升非常明显。如果你知道自己处理的音频都是中文就不要依赖自动语言检测直接把languagezh传进去。自动检测不仅要额外推理一轮还有可能误判特别是在音乐、静音较多的场景里。beam_size是Beam Search的宽度。默认值对应速度优先调高到5-8会提升精度但推理时间也会上涨。我实测下来对于普通话清晰、无背景音的录音beam_size设为2和5的差别不大但对方言口音或嘈杂环境调高beam_size能看到明显的文字连贯性改善。VAD语音活动检测是容易被忽略的好功能。它能在音频中过滤掉静音段和人声间隙避免模型在空白片段上产生幻觉文本。尤其是长音频里夹杂大段空白时开启VAD能显著降低输出中出现无意义重复内容的概率。代价是会增加一些前处理时间整体来说值得开启。并发方面faster-whisper的模型加载后每个转写请求都会占用一定内存CPU环境下多个请求同时到达会相互争抢算力实际吞吐反而更差。建议在业务侧控制并发数或者在前端加一个简单队列保证同时只有一个转写任务。有条件的话在compose里给容器限制cpus让Docker调度器帮忙控制资源竞争。4. 实操中高频出现的问题与排查4.1 容器里GPU不可见容器启动就报错典型症状宿主机上nvidia-smi正常但容器内报找不到CUDA设备或者服务启动时模型加载失败。这个问题的根源几乎都是GPU容器运行时没装好而不是代码问题。先验证容器能否访问GPUdocker run --rm --gpus all 你的基础镜像 nvidia-smi如果提示找不到GPU说明需要安装N卡容器工具包sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtimedocker sudo systemctl restart docker重启Docker后再跑上面的验证命令。能正常输出GPU信息后把compose里环境变量改成DEVICEcuda、COMPUTE_TYPEfloat16重新构建启动即可。注意宿主机显卡驱动版本要足够新否则即使容器运行时装好了驱动接口不匹配一样会报错。4.2 模型反复下载、下载慢一种很常见的情况是明明本机已经有模型文件了容器启动时还是往模型仓库下载几百MB甚至几个GB的数据。原因在于模型路径配置不对。WhisperModel接收的是一个本地目录路径时它就直接从本地加载接收的是字符串模型名时它会尝试从模型仓库拉取最新文件。所以compose里的WHISPER_MODEL_DIR必须指向挂载目录下的具体子目录而不是草草写一个/models了事。下载慢的问题我的建议是永远不要在容器运行时临时下载大模型。提前把模型下好、放到models目录里挂载进容器是最稳妥的方案。如果你有局域网内的其他机器已经下载过模型直接拷贝过来即可。容器启动后只要日志里没有关于模型仓库访问的记录说明走得是本地加载。4.3 中文乱码、JSON转义与编码问题转写结果里中文正常但终端打印出来是\uXXXX一类的转义序列或者日志里全是乱码。这种情况通常有两层原因一是容器内没有设置UTF-8语言环境二是在调用接口拿结果时没有正确解码。在compose的environment里加上LANGC.UTF-8和LC_ALLC.UTF-8可以让容器内日志和文件名处理更规范。接口层我建议在FastAPI返回JSON时保持中文原文不需要额外配置因为HTTP层面的UTF-8编码本来就是默认的。如果客户端是脚本请求响应后一定要按response.encoding或utf-8解码不要用系统默认编码尤其是Windows终端下容易乱码。4.4 内存不足、长音频转写卡死长音频转写时容器内存飙升甚至直接被OOM杀掉这是CPU部署里最常遇到的问题。原因是整段音频会一次性加载到内存里做特征提取而模型推理过程中还会额外占用大量临时缓冲。我在一台内存只有8G的机器上跑medium模型处理半小时的音频内存占用直接突破5G。解决办法很直接长音频先切分再转写。用ffmpeg把音频按60秒一段切成小文件逐段调用转写接口最后按时间戳拼接。Whisper对短音频的处理速度更快分段后内存峰值也大幅下降。另一种思路是限制容器内存上限让它因内存不足提前报错而不是悄悄拖垮宿主机。compose里mem_limit字段是垃圾回收的第一道防线。4.5 容器起不来、端口冲突与调参方向服务偶尔出现“端口已被占用”或者容器反复重启排查顺序我建议是先看日志再看端口占用最后看健康检查。docker compose logs --tail 100 sudo lsof -i :8000如果是端口被旧容器占用执行docker compose down清理后再up。如果日志显示模型加载失败先去确认models目录挂载路径和实际文件名是否一致。如果容器反复restart但日志又是空的多半是健康检查在模型加载完成之前就开始探测把start_period调大一点即可。还有一个容易被忽略的问题docker compose和docker-compose是两个不同的命令。旧版用连字符新版用空格。Ubuntu 24.04安装的Compose插件正确用法是docker compose如果你发现输入旧命令报错不用惊讶换空格版就行。整套部署跑下来我最想提醒的不是某个命令怎么写而是把“模型下载、模型加载、接口调用”这三个阶段拆开看。每个阶段独立验证通过后再进下一步能省掉大量串行排查的时间。我在实际操作中最满意的一个改动是把模型目录独立挂载、和代码完全分离后续换模型尺寸时只需要改环境变量指向新目录不需要重新构建镜像这个模式建议直接保留。部署起来之后日常使用中调整最多的其实就是beam size和VAD开关其他参数基本可以稳定不动。