这个博客已经运行了 11 年。从最早那台便宜到离谱的搬瓦工虚拟主机(内存只有几百 MB)开始,中间搬到过万网,万网后来被阿里收购,虚拟主机的价格也一路涨到一年 500 块这种离谱数字;后来又折腾去了轻量应用服务器,直到今天,又回归到最质朴的 ECS 主机——一个月八块钱,性能反而更好。

遥想当年折腾这些的时候,压根没有 AI 这种东西,所有资料都得自己在网上一点点搜,大部分还是从别人的博客里现学现卖的。如今 Web 技术发展得飞快,PHP + WordPress 这套组合看起来已经显得有点 old school 了,但这个博客还是安安静静地躺在某台服务器上,等着被搜索引擎收录,等着被人访问,然后由 PHP 和这个古老的数据库组合生成网页,呈现在访问者面前。这跟在社交平台上发一条动态是很不一样的体验——所有的搭建都是自己一手弄起来的,光是这件事本身,就还挺浪漫的。

所以昨天还半自言自语地问我对象:你说这服务器到底要不要续费?想了想,还是续了。现在的我对于每个月几块钱的服务器费用早就没什么经济压力了,只是单纯更喜欢这种自己搭建、自己发布内容的方式。而一篇篇博文也如实记录着我曾经的所思所想,这件事本身,就挺让人欣慰的。

这次续费之后,正好借着这个机会,把这套老掉牙的环境从 LNMP 一键包彻底搬到了 Docker Compose 上。整个过程踩了不少坑,记录下来,也算是给未来的自己(或者搜到这篇文章的人)留个参考。

起点:新的服务买什么呢?

旧的轻量应用到期了,2cpu+1g+30gb硬盘+2m带宽一年就要300+我觉得还是贵了,偶然发现同样2cpu+1g+10~20gb硬盘的ecs买5年话平均一年只要100+,只是流量每月20g免费,算了一下其实非常适合我这种情况,果断换这种!

LNMP 一键包翻车了

尝试使用ecs自带的装宝塔面板,好嘛说内存最少要1g,报错装不上。那直接装LNMP一件包呢?老服务器上跑的是 lnmp.org 那套一键安装脚本,新服务器打算原样照搬。

安装脚本是:

wget https://soft.lnmp.com/lnmp/lnmp2.2.tar.gz -O lnmp2.2.tar.gz && tar zxf lnmp2.2.tar.gz && cd lnmp2.2 && ./install.sh lnmp

结果脚本卡在下载源上,soft.lnmp.com 解析出来的地址链路最终却指向了 127.0.0.1,一看论坛一个月前就有人问了这个问题但是没有任何回复,更像是这个下载源自己已经停摆了。折腾了一圈无果,推断lnmp.org这个项目已经维护有限了,索性放弃了这条路。

转向 Docker Compose

与其继续跟一个可能已经没人维护的下载源较劲,不如直接上 Docker Compose——反正核心需求就三样:Nginx、PHP-FPM、MySQL。

第一版:先跑通再说

最初的思路很朴素,三个容器:

  • nginx:alpine 负责反向代理和静态文件
  • wordpress:php7.4-fpm 跑 PHP
  • mysql:5.7 存数据

配合一份docker-compose.yml 和一份 nginx 的 .conf,然后使用docker compose up -d来运行整套容器

第二版:加装 phpMyAdmin,方便维护数据库,还要解决SSL证书续签问题

加了一个 phpmyadmin 服务,但没有直接暴露在公网端口上,而是绑定在 127.0.0.1,通过 SSH 隧道(本地转发)访问:

ports:
  - "127.0.0.1:8081:80"

平时用 SSH 客户端(这次用的是 WindTerm)开一条本地转发隧道,映射到本地端口,再用浏览器访问 http://127.0.0.1:8081,比直接把管理面板暴露在公网上安全得多。

证书续签现在成了个问题,刚建博客的时候记得ssl免费证书都是一年一签的,现在改成了3个月,好在可以用certbot镜像来实现自动续签。

完整docker-compose.yml分享

version: "3.8"

services:
  db:
    image: mysql:5.7
    container_name: wp_mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: SsYQGyJ1
      # root 默认只允许从容器内部 localhost 登录,
      # 这里放开允许从其他容器(wordpress 服务)远程连接
      MYSQL_ROOT_HOST: '%'
      TZ: Asia/Shanghai
    volumes:
      - /root/wp-docker/mysql-data:/var/lib/mysql
    networks:
      - wp_net

  wordpress:
    image: wordpress:php7.4-fpm
    container_name: wp_app
    restart: always
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: *****
      WORDPRESS_DB_PASSWORD: *****
      WORDPRESS_TABLE_PREFIX: wp_1
      TZ: Asia/Shanghai
    volumes:
      - /home/wwwroot/winotmk.com/htdocs:/var/www/html
    networks:
      - wp_net

  phpmyadmin:
    image: phpmyadmin:5.2
    container_name: wp_pma
    restart: always
    depends_on:
      - db
    environment:
      PMA_HOST: db
      PMA_PORT: 3306
      UPLOAD_LIMIT: 64M
      TZ: Asia/Shanghai
    # 只监听在服务器本机,不直接对公网开放,
    # 想访问就用 SSH 隧道转发到本地,见下方说明
    ports:
      - "127.0.0.1:8081:80"
    networks:
      - wp_net

  nginx:
    image: nginx:alpine
    container_name: wp_nginx
    restart: always
    depends_on:
      - wordpress
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /home/wwwroot/winotmk.com/htdocs:/var/www/html:ro
      - /root/wp-docker/nginx/conf.d:/etc/nginx/conf.d:ro
      - /root/wp-docker/certbot/conf:/etc/letsencrypt:ro
      - /root/wp-docker/certbot/www:/var/www/certbot:ro
    networks:
      - wp_net

  certbot:
    image: certbot/certbot
    container_name: wp_certbot
    restart: always
    volumes:
      - /root/wp-docker/certbot/conf:/etc/letsencrypt
      - /root/wp-docker/certbot/www:/var/www/certbot
    # 每12小时检查一次,证书剩余不足30天才会真正续期,不会频繁请求
    entrypoint: >
      /bin/sh -c "trap exit TERM;
      while :; do certbot renew --webroot -w /var/www/certbot;
      sleep 12h & wait $${!}; done;"

networks:
  wp_net:
    driver: bridge

所以实际上这个compose跑了以下几个容器

  • 数据库mysql5.7
  • wordpress(含有php7.4)
  • phpmyadmin(数据库管理,需要用隧道连接然后127.0.0.1:8081端口)
  • nginx服务
  • certbot证书续费服务

这完整实现了以前LNMP包的功能(还有phpmysql管理和证书自动申请),遥想博客刚建立的时候docker听都没听过,也没有ai帮我一步到位给到这么好的方案

直接跑这个compose可能会遇到镜像拉取卡住问题,下面会提到配源以及docker cpmpose本身的安装问题

HTTPS 证书:两个方案的取舍

正好另一台服务器上跑着 Nginx Proxy Manager,自动续期 *.winotmk.com 的证书,一开始想着”直接把证书文件同步过来用不就行了”。

后来想了想,跨服务器同步证书文件挺麻烦——每次续期都要手动/定时同步一遍,多了一层依赖。干脆让这台服务器自己申请、自己续期,加一个 certbot 容器就够了,不用依赖任何外部服务器。

顺带弄明白了一件事:nginx 本身根本不负责证书申请这件事,它就是个按配置文件处理请求的软件,压根不知道什么是 ACME 协议。NPM 之所以看起来”自带续期功能”,其实是它内部集成了 certbot,加了一层管理界面把这两者粘合在了一起——原理上跟这次自己搭的 nginx + certbot 组合完全一样,只是少了个图形界面。

申请证书要分两步走

申请 HTTPS 证书本身,需要先有一个能正常工作的 HTTP 网站。因为 Let’s Encrypt 用 HTTP 验证域名归属,它会真的发一个请求去访问 http://你的域名/.well-known/acme-challenge/...,如果这时候连 HTTP 网站都没跑起来,验证根本无从谈起。

所以整个流程被拆成了两个阶段:

流程分两步:先用纯 HTTP 配置跑起来申请证书 → 拿到证书后切换成 HTTPS 配置。之后续期全自动

所以这里启动nginx有两个配置:

第一个配置

winotmk-step1-http.conf

# 第一阶段:申请证书用的纯 HTTP 配置
# 网站照常能通过 http 访问,同时给 certbot 验证用的路径开个口子
server {
    listen 80;
    server_name winotmk.com www.winotmk.com;

    root /var/www/html;
    index index.php index.html;

    client_max_body_size 64m;

    # Let's Encrypt 验证用,必须能被外网访问到
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass wordpress:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* /wp-content/uploads/.*\.php$ {
        deny all;
    }

    location ~ /\.ht {
        deny all;
    }
}

第二个配置

winotmk-step2-http.conf

# 第二阶段:拿到证书后启用这份配置(HTTPS)
# 使用方法见下方部署说明:拿到证书后,删掉 step1 文件,
# 把这个文件改名去掉 .disabled 后缀,然后 reload nginx

server {
    listen 80;
    server_name winotmk.com www.winotmk.com;

    # 续期验证依然要用这个路径,保留
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://winotmk.com$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name www.winotmk.com;

    ssl_certificate     /etc/letsencrypt/live/winotmk.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/winotmk.com/privkey.pem;

    return 301 https://winotmk.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name winotmk.com;

    ssl_certificate     /etc/letsencrypt/live/winotmk.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/winotmk.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    root /var/www/html;
    index index.php index.html;

    client_max_body_size 64m;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass wordpress:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS on;
        include fastcgi_params;
    }

    location ~* /wp-content/uploads/.*\.php$ {
        deny all;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 30d;
        access_log off;
    }

    location ~ /\.ht {
        deny all;
    }

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}

换了配置后nginx可以像这样重新载入

cd /root/wp-docker
docker compose exec nginx nginx -s reload

部署步骤

前提: 确认 winotmk.comwww.winotmk.com 的 DNS A 记录直接指向这台服务器的公网 IP(不是走 NPM 那台),因为 Let’s Encrypt 要能直接访问到这台服务器的 80 端口做验证。

1. 放置文件

mkdir -p /root/wp-docker/nginx/conf.d /root/wp-docker/certbot/conf /root/wp-docker/certbot/www

docker-compose.yml 放到 /root/wp-docker/winotmk-step1-http.conf 放到 /root/wp-docker/nginx/conf.d/(先只用这个文件,step2 那个先不用放进去,或者放进去但保持 .disabled 后缀,nginx 只加载 .conf 结尾的文件,不会理会它)。

2. 先用纯 HTTP 跑起来

cd /root/wp-docker
docker compose up -d

访问一下 https://winotmk.com,确认网站能正常打开(这一步能通,说明 80 端口和 DNS 都没问题,等下证书申请才能成功)。

3. 申请证书

cd /root/wp-docker
docker compose run --rm --entrypoint certbot certbot certonly \
  --webroot -w /var/www/certbot \
  -d winotmk.com -d www.winotmk.com \
  --email 你的邮箱@qq.com \
  --agree-tos --no-eff-email

跑成功会提示证书保存在 /etc/letsencrypt/live/winotmk.com/(对应宿主机 /root/wp-docker/certbot/conf/live/winotmk.com/)。

4. 切换到 HTTPS 配置

cd /root/wp-docker/nginx/conf.d
rm winotmk-step1-http.conf
mv winotmk-step2-ssl.conf.disabled winotmk-step2-ssl.conf
cd /root/wp-docker
docker compose exec nginx nginx -s reload

访问 https://winotmk.com,应该能看到锁头图标了。

5. 自动续期已经在跑了

certbot 那个容器会每 12 小时自动检查一次,证书快过期(剩 30 天内)才会真正续期,续期完文件会自动更新到共享的 /root/wp-docker/certbot/conf/ 目录。

唯一还差一步——nginx 不会自动感知证书文件变了,得 reload 一下才能加载新证书。加个每天跑一次的 cron:

6.证书续期后,nginx 不会自己感知

certbot 容器会按自己的节奏自动检查、自动续期,但 nginx 并不会主动感知证书文件变了,得手动或者定时 reload 一下才能吃到新证书。加了条 cron,每天固定 reload 一次,先打开配置文件:

crontab -e

我习惯用vim,在文件里加一行:

0 4 * * * cd /root/wp-docker && docker compose exec nginx nginx -s reload

:wq保存退出这样整套流程就完全自动化了,不用再依赖另一台服务器。

保存后可以验证一下有没有写进去:

crontab -l

中间的一堆基础设施小坑

搭建过程里还遇到几个跟 Docker 环境本身有关的问题,挨个记一下:

  • docker-compose 报错版本不支持:原来服务器上装的是老版本的独立 docker-compose(一代工具,命令带横杠),而不是新版 docker compose 插件(命令带空格)。两者是完全不同的两套东西,新版才认识 compose 文件里较新的语法。
  • apt install docker-compose-plugin 提示找不到包:说明 Docker 官方 APT 源压根没配置成功,得手动把 GPG key 和官方源都配好,再重新装一遍完整的 Docker 全家桶。
  • 镜像一直拉不下来,context deadline exceeded:国内服务器访问 Docker Hub 经常连不上,配一个国内镜像加速源(或者用阿里云自己的容器镜像加速器)立刻解决。

安装docker compose相关:

# 装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

# 加官方 GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 加官方源
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 更新并安装完整一套(包括 compose 插件)
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

这条命令会把 Docker 引擎和 compose 插件都装/更新到位

配置 Docker 镜像加速:

编辑(或新建)daemon 配置文件:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "registry-mirrors": [
    "https://docker.m.daocloud.io",
    "https://dockerproxy.com",
    "https://docker.1panel.live"
  ]
}
EOF

重启 Docker 让配置生效:

sudo systemctl daemon-reload
sudo systemctl restart docker

验证配置是否生效:

docker info | grep -A 5 "Registry Mirrors"

能看到你刚才写的那几个地址就说明生效了。

再重新拉取compose就可以了

cd /root/wp-docker
docker compose up -d

一些docker compose的tips:

-d 参数的作用

  • docker compose up(不带 -d)→ 前台运行,容器日志直接刷在你终端里,命令会一直”卡”在那不返回,这时候 Ctrl+C 确实会把整个 compose 项目的容器全部停掉。
  • docker compose up -d(带 -d)→ 后台运行,容器启动完命令立刻返回给你,把终端控制权还给你,容器在后台持续跑着,跟这个终端窗口/SSH 会话已经没关系了。这时候 Ctrl+C 什么都不会影响(因为命令早就执行完退出了,没东西可以打断)。

验证一下

跑完 docker compose up -d 后,你可以随便关掉这个 SSH 连接、断网、关电脑,容器都会继续在服务器上跑着。下次登录服务器随时可以检查状态:

cd /root/wp-docker
docker compose ps

能看到几个容器都是 Up 状态就说明没问题。

compose常用管理命令

docker compose logs -f          # 看实时日志(这个是前台的,Ctrl+C 退出只是退出看日志,不影响容器)
docker compose logs -f nginx    # 只看某个服务的日志
docker compose stop             # 停止所有容器(但不删除)
docker compose down             # 停止并删除容器(数据卷不受影响,因为挂载在宿主机目录)
docker compose restart nginx    # 重启单个服务

真正会让 compose 项目停掉的,只有明确执行 docker compose stopdocker compose down,或者服务器本身重启/关机(不过如果设置了 restart: always,服务器重启后 Docker 服务恢复时会自动把这些容器又拉起来)。

最后一个坑:无限重定向

一切看起来都跑通了,切换到 HTTPS 配置后,访问网站却收到 ERR_TOO_MANY_REDIRECTS

原因是经典的 nginx + PHP 组合坑:nginx 自己知道当前是 HTTPS 连接,但没有把这个信息透传给 PHP-FPM。WordPress 通过 $_SERVER['HTTPS'] 判断当前协议,nginx 不传这个参数的话,WordPress 会误以为自己还在 HTTP 下,于是主动跳转到 HTTPS;跳转后请求再次经过 nginx 转发给 PHP,PHP 又一次误判成 HTTP,又跳一次——无限循环。

另外也是因为因为wordpress的数据库里 wp_options 表的 siteurlhome 两个字段写的是www.winotmk.com

解决办法是在 PHP 处理块里显式声明这是 HTTPS 请求:

location ~ \.php$ {
    fastcgi_pass wordpress:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTPS on;
    include fastcgi_params;
}

另外也是因为因为wordpress的数据库里 wp_options 表的 siteurlhome 两个字段写的是www.winotmk.com,这会导致nginx把请求给到wordpress,然后wordpress又踢回到nginx来回跳了。

所以顺手也检查了一下数据库里 wp_options 表的 siteurlhome 两个字段,确保跟实际访问协议、域名保持一致。

检查解析的话可以用:

curl -I https://winotmk.com
curl -I https://winotmk.com

第二条应该会返回 301 跳转到 https,这是我们配置里设置的”http 自动跳 https”生效了。

收尾

折腾完这一圈,现在的架构大概是这样:

  • nginx:反向代理 + HTTPS 终止 + 静态资源缓存
  • wordpress(PHP-FPM):跑博客程序本体
  • db(MySQL):独立的 wordpress 库,专库专用
  • phpmyadmin:只在 SSH 隧道后面才能访问
  • certbot:自动申请、自动续期证书,nginx 每天定时 reload 感知新证书

整套东西加起来,每个月的服务器成本几块钱,比多年前那台”几百 MB 内存”的搬瓦工主机性能好了不知道多少倍,也比后来那个一年 500 的虚拟主机便宜太多。这个博客大概还会这样安安静静地跑下去,继续记录接下来的一些事情。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


*

关闭菜单