多容器协作实战:网络、数据共享与依赖管理


多容器协作实战:网络、数据共享与依赖管理

上一期我们介绍了 Docker Compose,知道了它能用一个文件定义多个服务,一键启动整个应用。但在实际开发中,多容器协作不只是“一起跑起来”那么简单,还有很多细节需要考虑:容器之间怎么通信才安全高效?数据怎么共享?如果一个服务依赖另一个,如何保证它启动时依赖已经就绪?今天咱们就深入这些话题,通过一个具体的例子,看看多容器协作中的常见问题和最佳实践。

场景设定:我们要搭一个博客系统

假设我们要用 Ghost(一个基于 Node.js 的开源博客平台)和 MySQL 数据库,再配一个 Nginx 做反向代理。这就有三个服务:ghostmysqlnginx。它们需要协作:

  • ghost 连接 mysql 存数据;
  • nginx 把请求转发给 ghost,同时可能 serve 静态文件;
  • 所有服务的日志、配置文件、上传的文件可能需要持久化或共享。

下面我们就用 Compose 来定义它们,并逐步解决协作中的问题。

第一步:基础 Compose 文件

先写一个初版 docker-compose.yml

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: ghost_mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostpass
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"   # 方便本地客户端连接,生产环境可以不暴露

  ghost:
    image: ghost:5-alpine
    container_name: ghost_blog
    restart: unless-stopped
    depends_on:
      - mysql
    environment:
      database__client: mysql
      database__connection__host: mysql
      database__connection__user: ghost
      database__connection__password: ghostpass
      database__connection__database: ghost
      url: http://localhost:8080   # 后续会用 Nginx 代理,这里暂时写本地
    volumes:
      - ghost_content:/var/lib/ghost/content
    ports:
      - "2368:2368"   # Ghost 默认端口

  nginx:
    image: nginx:alpine
    container_name: ghost_nginx
    restart: unless-stopped
    depends_on:
      - ghost
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ghost_content:/var/www/ghost/content   # 共享 Ghost 的 content 目录
    networks:
      - frontend
      - backend

volumes:
  mysql_data:
  ghost_content:

这个文件定义了三服务,用了两个命名卷:mysql_data 存数据库文件,ghost_content 存 Ghost 上传的图片、主题等。nginx 额外挂载了本地的 nginx.conf 配置文件,并且把 ghost_content 卷也挂载到自己目录下,这样 Nginx 可以直接 serve 静态资源。

容器网络:服务名就是 hostname

注意 ghost 的环境变量里,database__connection__host: mysql,这里直接用了服务名 mysql。这是因为 Compose 会自动创建一个默认网络,并把所有服务加入其中,同时 DNS 会把服务名解析成对应容器的 IP。这就是服务发现的基础——你不需要手动写死 IP。

但有时候你可能希望控制网络拓扑,比如把 Nginx 放在一个对外网络(frontend),数据库放在内部网络(backend),只让 ghost 能访问两者。上面例子中我们在 nginx 服务下加了 networks 定义,但在顶层还没定义网络。完善一下:

networks:
  frontend:
  backend:

services:
  mysql:
    ...
    networks:
      - backend

  ghost:
    ...
    networks:
      - backend
    # ghost 不需要前端网络,除非你需要直接访问它

  nginx:
    ...
    networks:
      - frontend
      - backend

这样,Nginx 在两个网络里,可以代理 ghost(通过 backend 网络),而外界只能通过 Nginx 暴露的端口访问,ghostmysql 完全不暴露端口给宿主机,更加安全。

数据共享:多个容器挂载同一个卷

上面我们把 ghost_content 卷同时挂载到了 ghostnginx 容器。这是多容器共享数据的常用方式。ghost 容器往 /var/lib/ghost/content 写入上传的图片,Nginx 容器在 /var/www/ghost/content 下也能看到同样的文件,这样就可以直接让 Nginx 处理静态文件请求,减轻 Node 的压力。

注意:卷是 Docker 管理的,多个容器同时读写时要注意文件锁问题。这里 Ghost 主要是写入,Nginx 读取,没有问题。

依赖与启动顺序:depends_on 不够用

我们在 ghostnginx 里用了 depends_on,它能保证容器启动的顺序(先 mysql,再 ghost,最后 nginx)。但这只保证容器启动了,不保证里面的服务已经就绪。比如 mysql 容器起来了,但 MySQL 服务可能还在初始化,这时 ghost 连接数据库就会失败。

解决办法是:在应用层面添加等待机制。常见的有:

  • 使用脚本(如 wait-for-it.shdockerize)在容器启动时等待依赖服务端口可用。
  • 使用健康检查(healthcheck)结合 depends_oncondition 特性(Compose 3.8+ 支持)。

例如,给 mysql 加健康检查:

mysql:
  image: mysql:8.0
  ...
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
    timeout: 20s
    retries: 10
    interval: 10s

然后在 ghostdepends_on 里指定条件:

ghost:
  ...
  depends_on:
    mysql:
      condition: service_healthy

这样,只有当 mysql 健康检查通过后,才会启动 ghost。同样可以给 ghost 加健康检查,让 nginx 等待它。

环境变量与配置管理

上面我们把数据库密码直接写在了 Compose 文件里,这不安全。更好的做法是用 .env 文件或 Docker secrets。Compose 支持从 .env 文件读取变量。在项目目录创建 .env

MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_PASSWORD=ghostpass

然后在 Compose 文件中引用:

mysql:
  environment:
    MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    MYSQL_PASSWORD: ${MYSQL_PASSWORD}

注意,这个 .env 文件不要提交到代码仓库,加进 .gitignore。对于更敏感的场景,可以使用 Docker swarm 的 secrets 或者外部密钥管理工具。

与外界通信:端口映射与反向代理

上面例子中,我们只暴露了 Nginx 的 80 端口到宿主机,ghostmysql 都没有对外暴露端口。Nginx 通过 backend 网络访问 ghost 的 2368 端口。Nginx 配置文件 nginx.conf 里可以这样写:

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://ghost:2368;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /content/images/ {
        alias /var/www/ghost/content/images/;
    }
}

注意 proxy_pass 里的 ghost 就是服务名,会被解析为 ghost 容器的 IP。

更复杂的场景:多个服务副本与负载均衡

如果某个服务需要水平扩展(比如多个 ghost 实例),Compose 可以用 scale 或在 docker-compose up 时加 --scale ghost=3。但这时候服务名 ghost 会解析到多个容器 IP,请求会以轮询方式分发,需要保证应用是无状态的(比如 session 存外部存储)。Nginx 作为反向代理时,可以配置 upstream 来实现负载均衡。

例如在 nginx.conf 里:

upstream ghost_upstream {
    server ghost:2368;   # 这里的 ghost 会被解析为多个 IP
}

server {
    location / {
        proxy_pass http://ghost_upstream;
    }
}

但要注意,默认的 DNS 解析可能不会自动更新 IP 列表,如果容器动态变化,可能需要使用如 resolver 配合 set 等方式,或者用服务发现工具。Compose 在单机环境下 scale 相对简单,但生产环境建议用 Swarm 或 K8s。

小结

通过这个 Ghost + MySQL + Nginx 的例子,我们看到了多容器协作的几个关键点:

  • 网络:用自定义网络隔离服务,通过服务名进行通信。
  • 数据共享:使用命名卷让多个容器读写同一份数据。
  • 启动控制:依赖关系 + 健康检查确保服务顺序。
  • 配置管理:用环境变量和 .env 文件避免敏感信息硬编码。
  • 对外暴露:统一入口(Nginx)代理内部服务,提升安全性。

Docker Compose 让这一切变得简单,但也有些局限(比如跨主机编排)。不过对于开发、测试和小型生产环境,它已经足够强大。

现在你可以自己尝试搭建一个多服务应用,在实践中体会这些技巧。如果遇到问题,记得容器日志是你的好朋友(docker-compose logs)。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 多容器协作实战:网络、数据共享与依赖管理


Carpe Diem and Do what I like