跳到主要内容

5 篇博文 含有标签「Docker」

查看所有标签

数据库认证失败?WSL 下 Docker 静默占用了你的端口

· 阅读需 3 分钟

TL;DR

WSL2 环境下 Docker Desktop 静默占用 5432 端口。SSH 隧道 localhost:5432 实际连接的是 Docker 容器内的 PostgreSQL,而非远程服务器。密码认证失败的报错具有误导性——密码没错,连的实例错了。解决方案:隧道改用本地 5433 端口,配合 .env.local 隔离开发配置。


问题现象

通过 SSH 隧道连接远程 PostgreSQL 时报错:

PostgresError: password authentication failed for user "postgres"
severity: 'FATAL'
code: '28P01'
file: 'auth.c'
line: '329'
routine: 'auth_failed'

隧道命令看起来正常建立:

ssh -L 5432:localhost:5432 -L 3003:localhost:3003 user@server -N &

隧道没有报错,但连接后始终提示密码错误。确认密码完全正确,远程服务器上直接登录没有问题。


根因

WSL2 网络架构中,Docker Desktop 会在 WSL2 内部创建虚拟网络接口。当 Docker 容器映射了 5432:5432 时,Docker 在 WSL2 网络层监听 5432 端口。

SSH 隧道 -L 5432:localhost:5432 的含义是:将本地 5432 端口转发到远程服务器的 5432。但本地 5432 已被 Docker 占用,隧道的绑定静默失败——连接直接被 Docker 拦截。

结果:localhost:5432 实际连接到 Docker 容器内的 PostgreSQL。该实例有不同的用户密码配置,因此报 password authentication failed

这个错误的迷惑性:报错信息是"密码错误",不是"端口被占用"或"隧道失败"。开发者会反复确认密码,而真正的问题是连错了机器。


解决方案

Step 1:确认端口冲突

# WSL2 内检查
ss -tlnp | grep 5432

WSL 内可能查不到(Docker 端口从 Windows 侧映射),去 Windows PowerShell 交叉验证:

netstat -ano | findstr :5432

看到 docker-proxy 进程占用 5432,冲突确认。

Step 2:改用其他本地端口建隧道

# Before: 本地 5432(与 Docker 冲突)
ssh -L 5432:localhost:5432 user@server -N &

# After: 本地 5433(无冲突)
ssh -L 5433:localhost:5432 user@server -N &

-L 格式是 本地端口:远程主机:远程端口。只改本地端口,远程不受影响。

Step 3:创建 .env.local 隔离开发配置

# server/.env.local(加入 .gitignore)
DATABASE_URL=postgresql://postgres:your_password@localhost:5433/your_db

Step 4:dotenv 优先加载 .env.local

// Before:
import 'dotenv/config';

// After:
import dotenv from 'dotenv';
import { existsSync } from 'fs';
import { resolve } from 'path';

if (existsSync(resolve(__dirname, '../.env.local'))) {
dotenv.config({ path: resolve(__dirname, '../.env.local') });
} else {
dotenv.config();
}

开发环境用 .env.local(5433),生产继续用 .env(5432),互不干扰。


注意事项

报错信息可能误导你

password authentication failed 不一定代表密码错误。连到错误的实例(如 Docker 内的 PostgreSQL),该实例没有对应账户,同样报这个错。密码确认无误但仍失败时,优先排查连接目标。

不要改应用代码中的默认端口

在隧道层面换本地端口,配合 .env.local 管理配置。不要把代码里的默认端口改成 5433——那会影响生产和容器内连接。环境差异通过环境变量解决。

WSL 端口排查要查 Windows 侧

Docker Desktop 在 WSL2 模式下的容器端口映射直接出现在 Windows 网络层,WSL 内 sslsof 查不到 PID。遇到莫名端口占用,去 Windows 侧 netstat -ano 定位。


容器端口在 WSL2 里访问不到?Docker Desktop 的 network_mode:host 陷阱

· 阅读需 4 分钟

在为客户搭建 AI 数据分析平台(Airflow + PostgreSQL)的本地开发环境时遇到此问题,记录根因与解法。

TL;DR

Docker Desktop for Windows (WSL2 backend) 下,network_mode: host 的容器端口无法从 WSL2 宿主机访问。容器内 ss 显示端口在监听,但宿主机 curl localhost:PORT connection refused。原因是 host 模式共享的是 Docker 内部工具 VM 的网络,不是 WSL2 的网络。解法:override 文件中用 network_mode: !reset 移除 host 模式,改用 bridge + external network + 端口映射。

问题现象

项目使用 docker-compose.yml 部署 Airflow,base 配置:

x-airflow-common:
&airflow-common
image: airflow-ai-dag:latest
network_mode: host # 服务器上正常,WSL2 上出问题
volumes:
- ..:/opt/airflow/project

services:
airflow-webserver:
<<: *airflow-common
command: webserver
environment:
- AIRFLOW__WEBSERVER__WEB_SERVER_PORT=8082

容器启动正常,日志确认端口监听:

[INFO] Listening at: http://0.0.0.0:8082

但宿主机完全不可达:

$ ss -tlnp | grep 8082
# 空,无监听

$ curl localhost:8082/health
curl: (7) Failed to connect to localhost port 8082

而同一环境下,pgAdmin 容器用 -p 8080:80 端口映射,localhost:8080 正常访问。

根因

Docker Desktop for Windows 的网络架构与 Linux 原生 Docker 不同:

Windows 浏览器

WSL2 宿主机(你操作的终端)
↕ Docker Desktop 管道
Docker Desktop 工具 VM ← network_mode: host 的 "host" 是这里

容器

Linux 服务器上 network_mode: host 直接共享宿主机网络,容器端口 = 宿主机端口。但在 Docker Desktop WSL2 环境下:

  • host 模式的 "host" = Docker Desktop 工具 VM,不是 WSL2
  • 容器内 /proc/net/tcp 有端口监听记录
  • WSL2 宿主机的 /proc/net/tcp 没有对应记录
  • -p 端口映射的容器不受影响(Docker Desktop 会自动转发)

验证方法: 对比容器内外网络命名空间:

# 容器内:8082 (hex 1F92) 在监听
$ docker exec webserver grep '1F92' /proc/net/tcp
4: 00000000:1F92 00000000:0000 0A ...

# WSL2 宿主机:8082 不存在
$ grep '1F92' /proc/net/tcp
# 无输出

解决方案

步骤 1:创建 docker-compose.override.yml

!reset 移除 base 的 network_mode: host(需要 Compose v2.24+):

# docker-compose.override.yml
services:
airflow-webserver:
network_mode: !reset # 移除 base 的 host 模式
networks:
- app_network # 加入外部网络
ports:
- "8082:8082" # 端口映射
environment:
- DB_HOST=postgres-db # 用容器名连接数据库

airflow-scheduler:
network_mode: !reset
networks:
- app_network
environment:
- DB_HOST=postgres-db

networks:
app_network:
external: true # 引用已存在的外部网络

步骤 2:确保数据库容器在同一网络

# 查看数据库容器所在网络
$ docker inspect postgres-db --format '{{json .NetworkSettings.Networks}}'
# {"app_network": {...}}

# 确认外部网络存在(不存在则创建)
$ docker network create app_network # 或已存在则跳过

步骤 3:重启生效

$ docker compose down
$ docker compose up -d

验证:

$ ss -tlnp | grep 8082
LISTEN 0 4096 *:8082 *:*

$ curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/health
200

关键点:!reset 必须放在服务定义里

以下写法不生效

# ❌ 错误:放在 anchor 内
x-airflow-local:
&airflow-local
network_mode: !reset # 不生效!
networks:
- app_network

services:
airflow-webserver:
<<: *airflow-local # merge 时 base 的 network_mode:host 仍在

正确写法:

# ✅ 正确:每个服务单独写
services:
airflow-webserver:
network_mode: !reset # 必须在这里
networks:
- app_network

注意事项

  • network_mode: !reset 需要 Docker Compose v2.24+,用 docker compose version 确认
  • !reset 只能移除标量字段(如 network_mode),列表和字典字段不支持
  • 如果 base 使用了 YAML anchor(<<: *anchor),!reset 必须写在服务定义层,不能写在另一个 anchor 里
  • 改为 bridge 模式后,容器间不能用 localhost 互相访问,必须用容器名或加入同一 Docker network
  • network_modenetworks 互斥,同时存在会报 mutually exclusive 错误

修改 WordPress Block Theme 不生效?FSE 开发 5 大难题排查指南

· 阅读需 8 分钟

在为客户开发 WordPress Block Theme 时反复遇到这五个问题,每次排查都花了不少时间。整理成指南,帮助同样在做 FSE 开发的同学快速定位。

TL;DR

五个问题按频率排序:文件修改不生效(数据库缓存覆盖文件)、块嵌套错乱(注释未关闭)、子主题内容不渲染(缺少 post-content 块)、SVG 图标消失(WP_Filesystem 被插件污染)、WP-CLI 邮件失败(SMTP 插件在命令行不生效)。每个场景都给出可直接复用的排查命令。

场景一:改了主题文件,页面没变化

问题现象

修改了主题目录下的 theme.jsontemplates/*.htmlparts/*.html,刷新页面无变化。甚至 git pull 更新了代码,前端仍然显示旧样式。

根因

FSE 主题的模板和全局样式会被 Site Editor 保存到数据库——wp_templatewp_template_partwp_global_styles 三种自定义文章类型。WordPress 读取时数据库版本优先于文件版本。即使你改了文件,只要数据库里有对应记录,就会用数据库的。

解决方案

不同文件类型对应不同的清理方式:

修改内容清理方式
templates/*.htmlwp_template
parts/*.htmlwp_template_part
theme.jsonwp_global_styles + 缓存
patterns/*.php直接生效,无需清理

一键清理所有数据库模板缓存:

# 本地 Docker 环境
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template_part --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_global_styles --format=ids --allow-root) --force --allow-root'
docker exec wp_cli wp cache flush --allow-root

如果 theme.json 修改后仍不生效,先验证 JSON 格式是否有语法错误(如尾随逗号,JSON 规范不允许):

docker exec wp_cli wp eval 'echo json_encode(json_decode(file_get_contents(get_template_directory() . "/theme.json")));' --allow-root
# 返回空字符串 → JSON 有语法错误

注意事项

  • 开发期禁止在 Site Editor 中保存,避免产生数据库覆盖
  • 生产环境清理前确认 Site Editor 中没有自定义修改需要保留
  • patterns/*.php 不受此问题影响——Pattern 注册走 PHP 代码,不经过数据库
  • Site Editor 保存的 wp_global_styles 如果 JSON 损坏,会导致全站 WP_Theme_JSON_Resolver 解析错误,表现为所有页面样式崩溃

场景二:Site Editor 显示"尝试恢复",页面布局全乱了

问题现象

Site Editor 中 Pattern 显示"尝试恢复"(Attempt Recovery),保存后页面布局完全错乱。某些块被错误地嵌套在其他块内部,层级关系与源码不一致。

根因

WordPress 块编辑器使用 HTML 注释标记块的边界:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- 内容 -->
<!-- /wp:group -->

当容器块(如 wp:groupwp:columns)缺少关闭注释 <!-- /wp:group --> 时,Gutenberg 的 parse_blocks() 会将后续所有块视为该容器的子块。后果:

  1. 父块的 save 输出为空
  2. 触发 Block validation failed
  3. 后续所有块的嵌套关系全部错位

解决方案

排查:在浏览器控制台检查块树层级:

wp.data.select('core/block-editor').getBlocks()

检查返回的块树结构,确认每个容器块的 innerBlocks 是否符合预期。如果一个 group 块内部包含了不应该在里面的块,大概率是前面某个容器缺少关闭注释。

修复:打开 Pattern 源文件,逐个检查每个 <!-- wp:xxx --> 都有对应的 <!-- /wp:xxx -->。建议在编辑器中搜索 <!-- wp:<!-- /wp:,计数确认数量一致。

预防技巧

使用支持括号匹配的编辑器(如 VS Code),配合 Block Comment 高亮插件,可以在编写时立即发现未关闭的注释。对于复杂的 Pattern,建议先写骨架结构(所有开闭注释配对),再填充内容。

场景三:子主题覆盖模板后,编辑器内容消失了

问题现象

创建子主题覆盖父主题模板后,在 WordPress 页面编辑器中输入的内容(文本、图片等)在前端完全空白。但模板文件中硬编码的 Pattern(如 hero 区域、CTA 区块)正常显示。

根因

FSE 模板通过 <!-- wp:post-content /--> 块来渲染页面编辑器中的 post_content。如果子主题覆盖的模板文件中没有这个块,WordPress 不知道在哪里输出页面内容。

结果就是:模板的固定结构(header、hero、sidebar)正常显示,但编辑器里写的内容全部丢失。

解决方案

确保子主题模板包含 post-content 块:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">

<!-- 模板固定结构(hero、sidebar 等) -->

<!-- wp:post-content {"layout":{"type":"constrained"}} /-->

<!-- 更多固定结构(CTA、footer 引用等) -->

</div>
<!-- /wp:group -->

排查"改了没效果"时,按以下顺序确认:

  1. 模板文件是否包含 <!-- wp:post-content /-->
  2. 你修改的是模板文件还是页面内容——两者控制不同的内容区域
  3. 模板中内联的 cover block 由模板文件控制,与数据库 post_content 无关

场景四:SVG 图标突然消失,但文件还在

问题现象

主题中使用 WP_Filesystem 读取 SVG 图标文件,突然所有 SVG 图标消失。直接访问 SVG 文件 URL 返回正常内容,但页面上图标位置是空的。

根因

WordPress 的 $wp_filesystem 全局变量默认使用 WP_Filesystem_Direct(直接读写本地文件)。某些插件(备份、安全类)在初始化时会将 $wp_filesystem 替换为 WP_Filesystem_ftpsocketsWP_Filesystem_SSH2

FTP/SSH 适配器通过远程连接读取文件,对本地路径(如 /var/www/html/wp-content/themes/...)无法正确访问,返回空字符串。由于替换发生在全局作用域,所有使用 WP_Filesystem 的主题和插件代码都受影响。

解决方案

第一步——诊断:检查当前 $wp_filesystem 的实际类型:

# 本地 Docker 环境
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root

# 返回 WP_Filesystem_Direct → 正常
# 返回 WP_Filesystem_ftpsockets 或其他 → 已被污染

第二步——定位污染源:逐个禁用插件,检查哪个插件替换了适配器:

docker exec wp_cli wp plugin deactivate <plugin-name> --allow-root
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root
# 重复直到返回 WP_Filesystem_Direct

第三步——代码兜底:在主题中加 file_get_contents() 作为 fallback:

function mytheme_get_svg( $path ) {
global $wp_filesystem;

// 优先使用 WP_Filesystem
if ( $wp_filesystem && method_exists( $wp_filesystem, 'get_contents' ) ) {
$content = $wp_filesystem->get_contents( $path );
if ( $content ) {
return $content;
}
}

// WP_Filesystem 失败时回退到直接读取
if ( file_exists( $path ) ) {
return file_get_contents( $path );
}

return '';
}

注意事项

  • file_get_contents() 在某些受限主机可能被 disable_functions 禁用,但 VPS 和 Docker 环境通常可用
  • 根治方案是定位并处理污染源插件,代码兜底只是临时方案
  • 此问题具有隐蔽性——SVG 文件本身完好,直接访问正常,只有在 PHP 中通过 WP_Filesystem 读取时才返回空

场景五:命令行发邮件失败,网页端正常

问题现象

通过 wp eval 在命令行调用 wp_mail() 发送邮件,始终失败。但通过 Web 请求触发的邮件(用户注册、联系表单)发送正常。WP Mail SMTP 等插件已正确配置 SMTP。

根因

SMTP 插件通过 Hook 拦截 wp_mail(),将发信通道从 PHP sendmail 切换到 SMTP 服务。但这些插件的 Hook 注册依赖 WordPress 完整启动流程——特别是 wp_loaded 之后的阶段。

WP-CLI 的 wp eval 虽然加载了 WordPress 核心,但部分插件 Hook 在 CLI 环境下不会被注册。wp_mail() 回退到 PHP sendmail,而大多数服务器没有配置 sendmail,导致发送失败。

解决方案

方法一——Web 请求测试:在主题中临时添加测试路由,通过浏览器触发:

// 临时添加到 functions.php,测试完立即删除
add_action( 'wp_loaded', function() {
if ( ! isset( $_GET['test_mail'] ) ) return;
if ( '1' !== $_GET['test_mail'] ) return;

$result = wp_mail( '[email protected]', 'SMTP Test', 'Test email body' );
var_dump( $result ); // true = 发送成功
exit;
} );

访问 https://yoursite.com/?test_mail=1 触发测试。

方法二——eval-file 确保完整加载

cat > /tmp/test-smtp.php << 'EOF'
<?php
require_once ABSPATH . 'wp-load.php';
do_action('wp_loaded');

$result = wp_mail('[email protected]', 'CLI SMTP Test', 'Test body');
echo $result ? "Sent\n" : "Failed\n";
EOF

docker exec wp_cli wp eval-file /tmp/test-smtp.php --allow-root

最佳实践

生产环境中,邮件发送验证应通过 Web 请求测试。WP-CLI 适合定时任务和批量操作,但不适合验证依赖完整 WordPress Hook 链的功能(如邮件、缓存预热等)。


Docker 容器读不到宿主机文件?匿名 Volume 覆盖 Bind Mount

· 阅读需 3 分钟

在为客户部署 WordPress 站点时遇到此问题,记录根因与解法。

TL;DR

镜像 Dockerfile 中的 VOLUME 声明会创建匿名卷,挂载优先级高于 docker-compose.yml 的 bind mount。解决方案:停止容器 → 删除匿名卷 → 重启。

问题现象

CI 部署后,新增的静态文件或 PHP 代码在容器内不存在:

  • assets/images/logo.png 宿主机存在,容器内无 → Logo 不显示
  • inc/setup.php 新增 filter 代码,容器内是旧版本 → Filter 不生效
  • git pull 后文件已更新,容器内仍是旧内容

根因

WordPress 官方镜像的 Dockerfile 包含:

VOLUME /var/www/html

即使你的 docker-compose.yml 配置了 bind mount:

volumes:
- ./wordpress/wp-content:/var/www/html/wp-content

Docker 仍会为 VOLUME 声明的路径创建匿名卷,匿名卷的挂载优先级高于 bind mount,导致:

  1. /var/www/html 被匿名卷接管
  2. 你的 bind mount 只挂载到 /var/www/html/wp-content
  3. 但匿名卷已经"占领"了父目录,bind mount 实际上被覆盖

docker inspect 验证:

docker inspect prod_wordpress --format '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'

输出类似:

volume: /var/lib/docker/volumes/wp-prod_wp_html/_data -> /var/www/html# 匿名卷!
bind:/var/www/wp-prod/wordpress/wp-content -> /var/www/html/wp-content

解决方案

# 1. 停止容器
cd /var/www/wp-prod && docker compose down

# 2. 删除匿名卷
docker volume rm wp-prod_wp_html

# 3. 重启
docker compose up -d

验证

docker inspect prod_wordpress --format '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'

应只显示 bind mount,无匿名 volume:

bind: /var/www/wp-prod/wordpress/wp-content -> /var/www/html/wp-content

预防

使用 bind mount 部署时,检查镜像是否声明了 VOLUME。如果声明了:

  1. 首次启动前,确认没有残留的匿名卷
  2. 或者修改 docker-compose.yml,让 bind mount 路径与 VOLUME 路径一致(挂载到同一层级)

修复后,git pull 更新代码时容器自动读取新文件,无需 docker cp 或重启容器。

注意事项

  • 删除匿名卷前确保数据已备份或可通过git pull 恢复
  • 生产环境操作前先在测试环境验证
  • 如果容器内有关键数据,先用 docker cp 备份

在 Docker WordPress 容器中安装 WP-CLI

· 阅读需 2 分钟

TL;DR

官方 WordPress Docker 镜像不含 WP-CLI。在 docker-compose.yml 中添加 command 配置,容器启动时自动下载安装 WP-CLI,无需手动进入容器操作。

问题现象

在 WordPress Docker 容器内执行 wp 命令:

docker exec -it wordpress_container wp --version

报错:

bash: wp: command not found

根因

WordPress 官方 Docker 镜像基于 php:apache 构建,设计目标是保持镜像精简。WP-CLI 是独立的命令行工具,需要额外安装,不在默认镜像中。

解决方案

docker-compose.yml 的 WordPress 服务中添加 command,容器启动时自动安装 WP-CLI:

services:
wordpress:
image: wordpress:latest
volumes:
- ./wordpress:/var/www/html
command: >
bash -c "curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &&
chmod +x wp-cli.phar &&
mv wp-cli.phar /usr/local/bin/wp &&
docker-entrypoint.sh apache2-foreground"

关键点:

  1. curl -sO - 静默下载 WP-CLI phar 包
  2. chmod +x - 添加执行权限
  3. mv ... /usr/local/bin/wp - 移动到 PATH 目录,使命令全局可用
  4. docker-entrypoint.sh apache2-foreground - 执行原镜像的默认入口点,启动 Apache

重启容器后验证:

docker-compose down
docker-compose up -d
docker exec -it wordpress_container wp --version
# 输出: WP-CLI 2.x.x

对类似需求感兴趣?联系合作