Docker 容器时区、权限、网络的三个经典坑

Docker · 7 分钟阅读· 8 次阅读

这三个问题我在不同项目里遇到过至少十次,每次都要重新查。整理成一篇,以后直接翻。

坑一:日志时间差 8 小时

现象:容器里 date 显示 UTC,日志时间比本地时间早 8 小时。

原因:容器默认不继承宿主机的时区,基础镜像(尤其 debian:slim、alpine)里 /etc/localtime 是 UTC。

三种解法,按推荐程度:

方案 A:挂载时区文件(最简单)

services:
  app:
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

前提是宿主机时区正确(timedatectl 确认)。

方案 B:设 TZ 环境变量

environment:
  - TZ=Asia/Shanghai

这依赖程序自己读 TZ。Java、Python(配了 tzdata)、Node 都支持;部分 C 程序不读。

方案 C:镜像里装 tzdata

RUN apt-get update && apt-get install -y --no-install-recommends tzdata \
 && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
 && echo "Asia/Shanghai" > /etc/timezone \
 && rm -rf /var/lib/apt/lists/*

Alpine 要用 apk add tzdata。

推荐 A + B 一起:A 保证系统调用正确,B 保证程序读到的环境变量正确。

有个细节要注意:MySQL 容器的时间问题不在容器,在 mysqld 的 time_zone 变量。即使容器时区对了,MySQL 默认还是 SYSTEM,而 JDBC 连接时可能按 UTC 解释。稳妥做法是在配置里写死:

[mysqld]
default-time-zone = '+08:00'

坑二:挂载目录写不进去

现象:容器启动后报 Permission denied 或者 Read-only file system。

三个不同原因,要分清:

原因 1:UID/GID 不匹配

容器里的进程以某个 UID 运行(比如 nginx 的 www-data = 33),宿主机目录的属主是另一个 UID。

# 看容器里进程的 UID
docker exec -it myapp id

# 看宿主机目录的属主
ls -ld /data/myapp

解法一:把宿主机目录 chown 成容器里的 UID:

sudo chown -R 33:33 /data/myapp

解法二:在 compose 里指定用户:

user: "1000:1000"

但要注意,容器内的程序如果有配置文件读写、日志目录,全部都会受这个 UID 影响。

解法三(最省心):用命名卷而不是绑定挂载。命名卷由 Docker 管理,权限自动匹配。

原因 2:SELinux / AppArmor

CentOS/RHEL 系的机器上,SELinux 会阻止容器访问宿主机目录,即使 ls -l 看起来权限没问题。

getenforce
# Enforcing

解法:给目录打上容器标签。

sudo chcon -Rt svirt_sandbox_file_t /data/myapp
# 或
sudo semanage fcontext -a -t container_file_t "/data/myapp(/.*)?"
sudo restorecon -Rv /data/myapp

临时验证可以 setenforce 0,但不要长期关。

原因 3:根文件系统只读

read_only: true

或者 docker run --read-only。这时候需要给可写目录显式挂 tmpfs:

read_only: true
tmpfs:
  - /tmp
  - /run

坑三:容器访问不到宿主机服务

现象:宿主机上跑着一个服务(比如 MySQL 监听 127.0.0.1:3306),容器里连 localhost:3306 失败,连 172.17.0.1:3306 也失败。

原因:监听在 127.0.0.1 上的服务,只有本机回环能访问。Docker 的网桥(172.17.0.1)不是回环。

解法,从好到差:

方案 A:让服务监听 0.0.0.0,然后用防火墙限制

# my.cnf
bind-address = 0.0.0.0
# 只允许 docker 网段
sudo iptables -A INPUT -i docker0 -p tcp --dport 3306 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 3306 -j DROP

方案 B:用 host 网络

network_mode: host

容器直接用宿主机的网络栈,127.0.0.1 就是宿主机。代价是端口不能映射、容器间隔离消失。

方案 C:host.docker.internal

Docker Desktop(Mac/Windows)自带这个域名。Linux 上要手动加:

extra_hosts:
  - "host.docker.internal:host-gateway"

注意这依然要求服务监听在 0.0.0.0 或 docker0 的地址上——host-gateway 解析到的是 172.17.0.1。

附:容器间互相访问

同一个自定义网络里,用服务名当主机名:

services:
  web:
    networks: [backend]
  db:
    networks: [backend]
networks:
  backend:

web 里直接连 db:3306 就行。注意默认的 bridge 网络不支持 DNS 解析服务名,必须自定义网络。

一个小工具:进容器排查

# 用同一个网络起一个临时容器,带工具
docker run --rm -it --network myapp_backend nicolaka/netshoot

# 里面什么都有:dig、curl、tcpdump、ss、mtr

这个比在业务容器里 apt install 一圈干净得多。

最后

这三个坑的共同点是:Docker 的默认行为在很多地方和「宿主机上的进程」不一样,而文档不会把所有差异列出来。

我的习惯是新建一个 compose 项目时,先把这三件事确认一遍:时区挂载、目录属主、网络 DNS。确认过一遍,后面省很多事。

评论 (0)

还没有评论,来说两句吧~

发表评论

验证码

提交后需站长审核,通过后才会显示。