Docker 容器时区、权限、网络的三个经典坑
这三个问题我在不同项目里遇到过至少十次,每次都要重新查。整理成一篇,以后直接翻。
坑一:日志时间差 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)
还没有评论,来说两句吧~
发表评论