2.1 单服务应用部署


2.1 单服务应用部署

本节摘要:单服务部署是指用一个容器跑完一个完整应用,是 Compose 里配置量最小、心智负担最低的模式。本节先给三个"单服务就够"的判定信号,再给出静态站点的完整配方与逐行解读,随后与 docker run 对比说明 Compose 的价值,最后列出镜像标签、端口、卷、环境变量、重启策略的常用写法与常见坑。适合个人工具、无状态服务和原型验证三类场景,也是多服务部署的地基。

核心问题

  1. 能判断一个应用是否适合单服务部署,而不是盲目上多服务编排。
  2. 能写出静态站点或小型 API 的完整 docker-compose.yml 并逐行解释。
  3. 能说清 Compose 与 docker run 在启动、停止、配置管理上的差异。
  4. 能正确选择镜像标签、端口映射、卷类型与重启策略。
  5. 能用 docker stats 与 docker logs 完成单服务的日常监控与排障。

一、什么时候单服务就够

先泼一盆冷水:Compose 的多服务能力很诱人,但多数内部小工具根本用不上。我们的经验是,满足下面任意一条,单服务就是正确答案。

个人工具。 自己用的导航页、内网 wiki、定时脚本、转码服务,跑在一个容器里,一个 docker-compose.yml 文件管理,几秒钟就能起停。为这种场景拆三个服务,纯属给自己找运维活干。

无状态服务。 不保存用户数据、会话可以随时丢的服务,比如静态站点、API 网关、健康检查探针。这类服务连卷都不需要挂,容器删了重建,行为完全一致,单服务模式把复杂度压到了最低。

原型验证。 想快速验证一个想法能不能跑通,比如临时起一个 Nginx 看看前端打包结果、起一个 Redis 试试缓存方案。验证阶段最怕环境装半天,单服务加一条 up 命令,十分钟内就能看到结果。

反过来,什么时候不该用单服务?应用一旦出现两个以上需要独立升级、独立伸缩的进程,或者数据必须跨容器存活,就该跳到 2.2 和 2.3 的模式。判断标准不是"服务多不多",而是"拆开有没有收益"。

二、完整示例:静态站点

假设我们有一个静态网站,目录里只有 index.html,用官方 Nginx 镜像把它跑起来。这是单服务最标准的配方。

version: '3.8' services: web: image: nginx:1.21 ports: - "80:80" volumes: - ./html:/usr/share/nginx/html restart: always

逐行解读这份配置:

  • version: '3.8':声明 Compose 文件格式版本。新版 Docker 已经可以不写这行,但我们习惯保留,方便老环境直接照抄。
  • image: nginx:1.21:指定镜像和标签。我们刻意不用 latest,理由见第四节。
  • ports: "80:80":宿主机 80 端口映射到容器 80 端口,冒号左边是宿主机、右边是容器。
  • volumes: ./html:/usr/share/nginx/html:把宿主机当前目录下的 html 目录挂进容器的站点根目录,改完 index.html 刷新浏览器就能看到,不用重建镜像。
  • restart: always:容器异常退出后自动拉起。个人服务器上没人盯着,这条几乎必配。

部署步骤只有三步:

mkdir html mv index.html html/ docker-compose up -d

浏览器打开本机地址就能看到页面。-d 表示后台运行;要停掉整个项目,一条 docker-compose down 即可,容器、网络一并清理。

这套配方稍微改两处就能变成小型 API:把镜像换成自己构建的,比如 build: . 配合同级目录的 Dockerfile;再把端口换成应用的监听端口。配置骨架完全不变。

三、与 docker run 的对比

同一个静态站点,不用 Compose 也能跑:docker run -d -p 80:80 -v $(pwd)/html:/usr/share/nginx/html --restart always nginx:1.21。那 Compose 的价值在哪?对比一下就清楚了:

维度 docker run docker compose
配置载体 命令行参数,一长串难读 声明式 yaml 文件,可注释可评审
启动命令 每次重敲全部参数 up 一条命令,配置即状态
停止清理 手动记容器名再 rm down 统一清理容器与网络
多人协作 靠口头传递命令 文件进版本库,diff 可见
扩展能力 单容器孤立 随时升级为多服务、加卷、加网络

我们的态度很明确:docker run 适合在服务器上临时拉个镜像验证,凡是需要重复部署两次以上的东西,一律落成 compose 文件。文件本身就是文档,三个月后回来维护,看配置比回忆命令可靠得多。

docker run 还有一个隐性成本:命令行没有版本概念,历史命令改了就是改了;compose 文件可以 diff 出每一次配置变更,回滚也只是切回旧版本的问题。

四、常用配置项速览

单服务文件里常用的键不多,但每个都有讲究。

image。 公共镜像认准官方或社区认可的组织,比如 nginx、redis:alpine。自己构建的镜像用 build: . 指定 Dockerfile 所在目录。标签必须带版本号,nginx:1.21nginx:latest 稳——latest 指向的版本随时会变,今天能起明天可能起不来,生产环境尤其不能赌。

ports。 单端口映射 "80:80" 最常见;端口范围可以一次映射一串,比如 "8000-8001:8000-8001",适用于测试工具一次性暴露多个端口。如果只是容器间通信、不需要宿主机访问,用 expose 声明端口即可,它不产生映射,纯粹是文档性质的声明。

volumes。 三种挂法各有去处:宿主机目录 ./data:/var/lib/mysql 适合开发期直接看数据文件;命名卷 my-data:/var/lib/mysql 由 Docker 托管,删容器不删数据,是数据库的推荐选择;只读挂载 ./config:/etc/nginx/conf.d:ro 适合把配置目录挂进去又防止容器内误改,尾部加 :ro 就是只读。

environment。 直接在文件里写 DATABASE_URL=mysql://user:password@host:port/database 简单直接,但密码类信息别这么干,改用 2.5 节的 .env 方案。环境变量在容器启动时注入,进程起来之后改不了,需要变更只能重建容器;反过来,镜像里的默认值要设计成可被环境变量覆盖的形态,应用代码读配置一律优先环境变量。

restart。 四种取值,语义差别不小:

取值 行为 适用场景
no 不自动重启 一次性任务
always 任何退出都重启 常驻服务,最常用
on-failure 仅非正常退出时重启 怕无限重启循环时
unless-stopped 除手动停止外总是重启 希望手动停后不复活

五、日常监控与日志

单服务也要看监控和日志,否则半夜挂了没人知道。

资源占用用 docker stats 看,它实时列出每个容器的 CPU、内存、网络与磁盘 IO,排查"谁把机器吃满了"最直接。再讲究一点,可以挂 Prometheus 加 Grafana 做指标采集与看板,把容器指标变成曲线。

日志用 docker logs <container_id> 看,加 -f 跟随输出,加 --tail 50 只看末尾五十行。容器日志默认走标准输出,写文件的应用要把日志打到 stdout 才会被 Docker 捕获。日志量大了之后,ELK 这类聚合工具可以把多台机器的日志收拢到一起检索,但单服务阶段用不上,别过度设计。

日志的另一个问题是增长:容器日志默认写进宿主机的 Docker 目录,长期运行会越攒越大。除了 docker logs 查看,还可以给日志驱动配置轮转,比如 json-file 驱动的 max-size 与 max-file 参数限制单个文件大小,防止磁盘被日志写满。个人服务器上这个坑很常见,提前设上限省心。

另一种单服务形态是一次性任务容器:跑批脚本、数据迁移、定时同步,启动执行完就退出,不需要 restart 策略。Compose 对这类服务同样适用,up 之后任务跑完容器自动退出,docker-compose ps -a 能看到 exited 状态,配合宿主机的 cron 或调度器按需启动。退出码留在容器里,脚本判断成功失败比裸跑宿主机进程更干净。

六、单服务容易踩的坑

latest 标签的漂移。 镜像更新后重新 up,拉到的可能是行为不同的新版本。固定到具体版本号,升级时显式改标签,回滚也有据可查。

restart 策略配错。 开发机配 always,改代码后想停容器,docker stop 一停它又自己起来,容易误以为"停不掉"。开发环境用 unless-stopped,或直接 no。

绑定挂载的权限。 宿主机目录挂进容器后,容器内进程用 root 之外的用户写文件可能没权限,详见 2.3 节的 UID 与 GID 讨论。

镜像越建越大。 自己写 Dockerfile 时记得加 .dockerignore,把 node_modules、.git、构建缓存排除在构建上下文之外,既省构建时间又省镜像体积。

资源没有上限。 单服务也要在配置里声明资源上限,deploy.resources 的 limits 段可以限制 CPU 与内存,防止某个应用的异常占用拖垮整台机器,这个写法在 2.7 节还会用到。

⚠️ 端口占用是最常见的启动失败原因:宿主机 80 端口被别的进程占着,up 会直接报 bind 错误。先 docker ps 看有没有旧容器,或 netstat -tlnp 查端口归属,比反复重启有效。排查顺序也有讲究:先看状态再看日志,ps 定位"有没有起",logs 定位"为什么退",别一上来就改配置。

💡 单服务文件保持"一文件一应用":一个目录只放一个 compose 文件。目录名就是项目名,多个单服务应用各占一个目录,互不干扰,也比塞进同一个文件好维护。

七、变体:用本地 Dockerfile 构建小型 API

公共镜像之外,自研应用要本地构建。compose 文件同级目录放 Dockerfile 与源码,把 image 换成 build:

version: '3.8' services: api: build: . ports: - "8000:8000" environment: - APP_ENV=dev restart: unless-stopped

build 指向包含 Dockerfile 的目录,up 时先构建镜像再启动容器。镜像构建过一次后,源码没变就不会重复构建;需要强制重建时给 up 加 --build 参数。build 与 image 可以同时出现:build 指定构建来源,image 给产物命名,便于推送到私有仓库。写完文件建议先跑 docker-compose config 校验一遍,它渲染出最终配置并检查语法错误,比直接 up 撞错误快得多。API 一旦需要数据库支撑,就超出单服务范畴,直接切到 2.2 节的模式。小型 API 的 Dockerfile 同样遵循最小化原则:多阶段构建把依赖安装与运行分离,运行时镜像只留产物,体积能小一个量级。

八、从单服务走向多服务的信号

三个信号出现任意一个,就该考虑拆分。第一,应用需要自己的数据库或缓存,且数据要长期留存,比如给 API 配 PostgreSQL;第二,业务里出现两个可以独立升级、独立重启的进程,比如 Web 服务与定时任务;第三,某一部分流量开始波动,需要单独伸缩。拆分不必一次到位:先把数据层挪出去,再挪中间件,每一步都用 2.6 节的健康检查守住依赖边界,过渡会平顺得多。单服务不是"低配",而是"刚好够用"——它永远是我们的默认起点,多服务是需求推着走出来的,不是为了显得专业。

本章回顾

  • 三个信号判断单服务:个人工具、无状态服务、原型验证,满足其一就够用。
  • 配方骨架固定:image 加 ports 加 volumes 加 restart,四行就能跑起一个静态站点。
  • Compose 胜在可复现:配置进版本库、命令可重复、升级可 diff,这是 docker run 给不了的。
  • 镜像标签用版本号:nginx:1.21 而非 nginx:latest,杜绝漂移。
  • restart 按语义选:常驻服务用 always,开发机用 unless-stopped,一次性任务用 no。
  • 监控日志别省:docker stats 看资源,docker logs 看输出,够覆盖单服务九成场景。
  • 权限问题提前想:绑定挂载的写权限、端口占用,都是启动期的头号杀手。

单服务是最小可行部署,但大多数真实应用跑着跑着就会长出第二个服务——下一节,我们看看 Web 与数据库如何在一个 compose 文件里协同工作。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U