惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
Vercel News
Vercel News
F
Fortinet All Blogs
月光博客
月光博客
G
Google Developers Blog
博客园 - Franky
GbyAI
GbyAI
The Cloudflare Blog
I
InfoQ
雷峰网
雷峰网
WordPress大学
WordPress大学
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
T
The Blog of Author Tim Ferriss
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 聂微东
小众软件
小众软件
腾讯CDC
B
Blog
量子位
V
V2EX
S
SegmentFault 最新的问题
Google DeepMind News
Google DeepMind News

博客园 - Swizard

还在被框架绑架?一文看懂“六边形架构”,让你的核心业务稳如泰山! 逐行解剖:扒开 Lovable Agent 源码,看顶级 AI 是如何“思考”与“动刀”的 还在无脑堆砌提示词?三分钟看懂 Vercel v0 价值千万的 System Prompt 底层逻辑 OpenClaw+OpenViking + NVIDIA API 配置教程 拒绝“挤牙膏”!用苏格拉底提示法,让 AI 从“复读机”进化为“咨询顾问” 别再写丑陋的 Shell 脚本了:用 Gum 给你的终端穿上“高定西装” 拒绝视觉噪音:用“数据墨水比”法则重塑你的专业图表 告别 Git Stash:用 Git Worktree 实现多任务并行,效率起飞! 手机里的 Linux 实验室:Termux 让你随时随地开启“黑客”模式 拒绝“隐形”!用 JSON-LD 让你的网页在 Google 搜索结果中“自带光环” ⚔️ Google SGE vs. Perplexity:一场内容争夺战,两套生存法则 SEO 已死?拥抱 AEO/GEO:如何让 AI 主动“推荐”你的网站 Python 重构神器:告别 ast 模块的痛苦,三行代码搞定复杂批量修改 别再手写递归找文件了!用 Python rglob 一行代码搞定海量搜索 告别笨重的 For 循环:用 xargs 打造 Linux 命令行流水线神器 告别“断网即崩溃”:像存档游戏一样管理你的终端会话(Tmux 终极入门) 沉默的观察者:Multi-Agent 架构如何实现“零指令”主动服务? 杀死那个提示词框:揭秘“意图识别 + 动态组装”的幕后逻辑 UI 设计如何落地“Boss Mode”?告别聊天框的四个交互法则 用 100 倍 Token 换取 10% 体验:关于 AI Agent 的“暴力美学”
告别 sudo 滥用!用 gosu 优雅解决 Docker 容器权限的“千古难题”
Swizard · 2026-02-11 · via 博客园 - Swizard

你是否在写 Dockerfile 时遇到过这种进退两难的尴尬局面?

场景 A:为了安全,你在 Dockerfile 里写了 USER app。结果容器启动时,应用因为没有权限创建日志文件或修改数据目录而直接崩溃。 场景 B:为了省事,你直接用 root 跑所有东西。虽然跑通了,但你的安全团队发来警告,或者在挂载宿主机卷(Volume)时,生成了一堆宿主机无法删除的 root 权限文件,搞得你焦头烂额。 场景 C:你想聪明一点,用 sudo 切换用户。结果发现容器无法优雅停止,信号(Signal)传不到应用进程,导致数据损坏。

如果这些场景让你感到膝盖中箭,那么请立刻停下手中的工作,花 5 分钟认识一下今天的主角——gosu

什么是 gosu?为什么要用它?

简单来说,gosu 是一个基于 Go 语言编写的工具,它的唯一使命就是:让你可以先以 root 身份做一些准备工作(比如修改文件权限),然后“降权”以指定用户的身份去执行最终的应用程序。

你可能会问:“这不就是 sudo 或者 su 吗?”

大错特错。

🍰 生活类比:换岗的保安

为了让你秒懂 su/sudogosu 的区别,我们来打个比方:

想象你经营一家银行(容器),你是银行经理(Root),拥有所有钥匙。你需要让**柜员(普通用户)**去坐柜台工作。

  1. 使用 susudo: 你(经理)并没有离开,而是站在柜员身后盯着他。如果有人来打劫(发送停止信号 SIGTERM),劫匪是对着你喊话。但因为你是个中间人,柜员可能根本听不到劫匪的话,还在继续数钱,结果被“撕票”(强制杀死进程,数据丢失)。

    • 技术解释sudo 会启动一个新的子进程。你的应用不是 PID 1,它收不到 Docker 发出的停止信号。

  2. 使用 gosu: 你(经理)把钥匙交给柜员,帮他把椅子调整好,然后原地变身成了柜员,或者直接消失,把位置完全让给柜员。现在柜台里只有柜员一个人。

    • 技术解释gosu 使用了 exec 系统调用。它会用应用程序的进程替换掉当前的 shell 进程。你的应用由于“篡位”成功,直接变成了 PID 1,能够完美接收并处理所有信号。

🛠️ 动手实战:从 Dirty 到 Clean

光说不练假把式。我们来看看如何在 Docker 中正确使用 gosu

1. 错误的写法(反面教材)

这是很多新手常犯的错误,试图用 su 切换用户:

#!/bin/bash
# entrypoint.sh

# 假设我们需要先把数据目录的所有权给 redis 用户
chown -R redis:redis /data

# 错误示范:使用 su
su redis -c "redis-server"

后果:当你运行 docker stop 时,Docker 会等待 10 秒然后强杀容器,因为 redis-server 收不到停止信号。

2. 正确的写法(Gosu 登场)

首先,我们需要在 Dockerfile 里安装 gosu

Dockerfile:

FROM ubuntu:22.04

# 1. 创建用户
RUN groupadd -r redis && useradd -r -g redis redis

# 2. 安装 gosu (这是一个标准安装模板,可以直接抄)
ENV GOSU_VERSION 1.16
RUN set -eux; \
	apt-get update; \
	apt-get install -y gosu; \
	rm -rf /var/lib/apt/lists/*; \
	# 验证一下安装是否成功
	gosu nobody true

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["redis-server"]

entrypoint.sh (核心逻辑):

#!/bin/bash
set -e

# $1 是 CMD 传入的第一个参数,比如 'redis-server'

# 判断:如果第一个参数是 'redis-server',且当前是 root 用户运行
if [ "$1" = 'redis-server' ] && [ "$(id -u)" = '0' ]; then
    # 步骤 A: 利用 root 权限修正文件归属
    # 这一步是 Dockerfile 里的 USER 指令做不到的!
    chown -R redis:redis /data
    
    # 步骤 B: 降权并执行
    # 注意前面的 exec!这是关键!
    # gosu redis "$@" 意思是:以 redis 用户身份执行后面的命令
    exec gosu redis "$@"
fi

# 如果不是 redis-server 或者已经不是 root,直接执行命令
exec "$@"

代码解析:为什么要这么写?

  1. [ "$(id -u)" = '0' ]: 脚本一开始是 Root 运行的(因为 Docker 默认是 Root)。我们需要这个权限来执行 chown

  2. chown ...: 这是我们在降权前必须完成的“脏活累活”。

  3. exec gosu redis "$@":

    • gosu redis: 告诉 gosu 切换到 redis 用户。

    • "$@": 代表 Dockerfile 中的 CMD (即 redis-server)。

    • exec: 这是一个 Linux 命令,意思是“用新进程替换当前进程”。执行完这行后,entrypoint.sh 这个 shell 脚本就彻底消失了,取而代之的是 redis-server 进程。redis-server 继承了 PID 1 的宝座

🌊 进阶深潜:常见陷阱与最佳实践

1. 为什么不直接在 Dockerfile 里用 USER 指令?

USER 指令当然好,但它太“死板”。一旦声明了 USER redis,之后的所有层(Layer)都是非 Root 的。如果你需要在容器启动的那一刻(Runtime)去修改挂载卷(Volume)的权限,USER 指令就无能为力了,因为那时你已经没有 Root 权限了。

最佳实践

  • 如果你的应用不需要在运行时修改文件权限,直接在 Dockerfile 结尾用 USER,这是最安全的。

  • 如果你的应用需要初始化数据目录(比如数据库),必须使用 Entrypoint脚本 + gosu 的模式。

2. 忘记加 exec

这是最容易犯的错误:

如果少了 execgosu 会作为子进程运行,雖然权限对了,但信号传递链条依然是断裂的(虽然 gosu 试图处理信号,但不如直接替换进程来得稳健)。请刻在脑门上:gosu 总是和 exec 成对出现。

🎯 总结 (TL;DR)

  1. 不要在 Docker 里用 sudo,它会破坏信号传递,导致僵尸进程或数据丢失。

  2. gosu 是 Docker 的最佳拍档,它允许你“Root 启动,非 Root 运行”。

  3. 核心公式Entrypoint (Root) -> chown/setup -> exec gosu user app