利用 Docker “一键”部署 Web 服务环境

发布于 12 天前  66 次阅读


一些废话

啊咧?又有一个家伙忘记续费 VPS 导致数据被删博客失联!
原因竟然是重装电脑系统但是没安装 Outlook 导致续费邮件发了一堆还长达半个月没被看见,最后还是群友提醒域名 SSL 认证失败访问不了博客才发现事情的严重性……
那么来着手重装 VPS 系统以及 Web 环境吧!

重装系统

由于 RackNerd VPS 提供安装的系统版本实在是太老旧,一番寻找发现有个很便捷项目:一键DD/重装脚本 (One-click reinstall OS on VPS)
以前在 VPS 上使用的系统都是 Ubuntu ,但最少有一个受不了的状况让我决定换掉:SSH 终端有订阅提醒以及广告……
在 Fedora 与 AlmaLinux 之间纠结了一阵,最后还是选择了天然带 LTS 属性的后者。

下载脚本:

curl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_

执行重装:

bash reinstall.sh almalinux --password PASSWORD --ssh-port PORT

直接设定新系统的 Root 密码与 SSH 端口,又节省大量细节操作,等两三分钟即重装完成且重启到新系统上面了。

新主机名:

hostnamectl set-hostname HOSTNAME

登录 SSH 发现主机名很长很奇怪,结果是临时的,那么直接设定一个固定的。

环境搭建

以前用的 Web 环境一键搭建脚本一直都是 OneinStack ,但早有听闻被收购恶意挂马投毒,所以也是不敢用了;我也不喜欢用面板,太商业化(基本收费),还是最小化手搓吧!

添加 Docker 仓库:

dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

清理缓存及更新组件:

dnf clean all && dnf upgrade -y

安装 Docker 组件:

dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

启动 Docker 服务:

systemctl enable --now docker

现在,Docker 已被启动和设为开机自启。

安装终端复用工具:

dnf install -y tmux

创建新的会话:

tmux new -s web

连接已有会话:

tmux attach -t web

这样就不怕 SSH 掉线重连后没有记录了。

现在我们来准备一个工作目录,执行:

mkdir -p /data/web/{caddy,php} && cd /data/web

预期目录结构:

/data/web/
├── .env                  # 环境变量文件
├── compose.yaml          # Compose 配置文件
├── caddy/
│   └── Caddyfile         # Caddy 配置文件
├── certs/                # SSL 配置持久化目录
├── import/               # SQL 文件初始化目录
├── mariadb/              # MariaDB 数据持久化目录
├── php/
│   └── Dockerfile        # 构建自定义 PHP 镜像的文件
├── sites/                # 网站根目录 (多站点存放处)
│   ├── example.com/      # 站点 A 源码
│   ├── test.com/         # 站点 B 源码
│   └── ...               # 更多站点
└── logs/                 # 日志统一存放目录
    ├── caddy/            # 访问日志
    ├── mariadb/          # 数据库日志
    ├── php-fpm/          # PHP 日志
    └── redis/            # Redis 日志

准备以下 4 个文件并放置在对应位置,其中 Caddyfile 仅供参考,其它文件均可开箱即用。

.env

# -----------------------------------------------------------
# 服务配置
# -----------------------------------------------------------

# 设置默认时区
TZ=Asia/Shanghai

# MariaDB 密码
MYSQL_ROOT_PASSWORD=mariadb

# -----------------------------------------------------------
# 镜像版本
# -----------------------------------------------------------

# Caddy
CADDY_VERSION=2.10.2

# Php
PHP_VERSION=8.5

# MariaDB
MARIADB_VERSION=11.8

# Redis
REDIS_VERSION=8.4

compose.yaml

# ===========================================================
# 🧩 Multi-Site PHP Stack
# -----------------------------------------------------------
# - 🌐 Caddy
# - ⚙️ Php-fpm
# - 🗄️ MariaDB
# - ⚡ Redis
# -----------------------------------------------------------
# Maintainer: galnetwen
# Updated: 2026-01-04
# ===========================================================

services:
  # ---------------------------------------------------------
  # - Caddy
  # - 自动 HTTPS + 极简配置
  # ---------------------------------------------------------
  caddy:
    image: caddy:${CADDY_VERSION}
    container_name: caddy
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    environment:
      TZ: ${TZ:-Asia/Shanghai}
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./logs/caddy:/var/log/caddy
      - ./caddy:/etc/caddy
      - ./certs:/data
      - ./sites:/srv/sites
    networks:
      - webnet

  # ---------------------------------------------------------
  # - Php-fpm
  # - 自定义 PHP 运行环境
  # ---------------------------------------------------------
  php-fpm:
    build:
      context: ./php
      args:
        PHP_VERSION: ${PHP_VERSION}
    image: galnetwen/php-fpm:${PHP_VERSION}
    container_name: php-fpm
    restart: unless-stopped
    environment:
      TZ: ${TZ:-Asia/Shanghai}
    volumes:
      - ./logs/php-fpm:/var/log/php-fpm
      - ./sites:/var/www/html
    networks:
      - webnet

  # ---------------------------------------------------------
  # - MariaDB
  # - 数据库管理系统
  # ---------------------------------------------------------
  mariadb:
    image: mariadb:${MARIADB_VERSION}
    container_name: mariadb
    restart: always
    environment:
      TZ: ${TZ:-Asia/Shanghai}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - ./logs/mariadb:/var/log/mysql
      - ./import:/docker-entrypoint-initdb.d
      - ./mariadb:/var/lib/mysql
    networks:
      - webnet

  # ---------------------------------------------------------
  # - Redis
  # - 缓存与会话存储
  # ---------------------------------------------------------
  redis:
    image: redis:${REDIS_VERSION}
    container_name: redis
    restart: always
    environment:
      TZ: ${TZ:-Asia/Shanghai}
    volumes:
      - ./logs/redis:/var/log/redis
    networks:
      - webnet

# -----------------------------------------------------------
# - Networks
# - 统一桥接网络名称供所有服务通信
# -----------------------------------------------------------
networks:
  webnet:
    driver: bridge
    name: webnet

Caddyfile
如果你想在调试时临时不用 HTTPS ,可以给域名末尾加上 :80 端口号,这样就不会自动申请证书及启用 HTTPS 。

# -----------------------------------------------------------
# 全局配置
# -----------------------------------------------------------
{
    # 证书关联邮箱
    email [email protected]

    # Cloudflare IP
    servers {
        trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
    }
}

# -----------------------------------------------------------
# 服务配置
# -----------------------------------------------------------
(main) {
    root * /srv/sites/{args[0]}

    encode zstd gzip

    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

        -Via
        -Server
        -X-Powered-By
    }

    @static path *.ico *.jpg *.jpeg *.png *.gif *.webp *.svg *.css *.js *.otf *.woff *.woff2 *.mp4
    header @static Cache-Control "public, max-age=31536000"

    php_fastcgi php-fpm:9000 {
        root /var/www/html/{args[0]}
        env CF-Connecting-IP {http.request.header.CF-Connecting-IP}
        capture_stderr
    }

    file_server

    log {
        output file /var/log/caddy/{args[0]}.log {
            roll_size 100MB
            roll_keep 4
        }
    }
}

# -----------------------------------------------------------
# 非法域名
# -----------------------------------------------------------
:80, :443 {
    # 直接关闭连接
    abort

    # 返回 403 错误
    # respond "Access Denied" 403
}

# -----------------------------------------------------------
# 站点配置
# -----------------------------------------------------------
haremu.com {
    @unallowed path /wp-config.php /xmlrpc.php /readme.html /license.txt
    respond @unallowed "Access Denied" 403

    @forbidden {
        path_regexp hidden ^/\.
        not path /.well-known/*
    }
    respond @forbidden "Access Denied" 403

    import main haremu.com
}

acg.sx {
    route {
        @folder path_regexp f ^/images/([^/]+)$
        rewrite @folder /images.php?folder={re.f.1}&{query}

        @textures path_regexp t ^/textures/([^/]+)$
        rewrite @textures /images.php?textures={re.t.1}&{query}

        rewrite /images /images.php?{query}

        @index path /
        redir @index /images 302
    }

    import main acg.sx
}

Dockerfile
直接取自 WordPress 官方,根本不用费脑子和担心编译失败,还会清理用完的编译工具来给镜像缩减体积。

# ===========================================================
# 🧩 Php-fpm for WordPress
# -----------------------------------------------------------
# - Base: Debian + Php-fpm
# - Extensions: mysqli, pdo_mysql, gd, zip, intl, exif, bcmath, opcache, redis
# -----------------------------------------------------------
# Source: https://github.com/docker-library/wordpress/blob/master/latest/php8.5/fpm/Dockerfile
# Updated: 2026-01-04
# ===========================================================

ARG PHP_VERSION

FROM php:${PHP_VERSION:-8.5}-fpm

LABEL maintainer="galnetwen" \
      description="Custom Php-fpm for WordPress based on Debian" \
      version="1.0"

# persistent dependencies
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        # Ghostscript is required for rendering PDF previews
        ghostscript \
    ; \
    rm -rf /var/lib/apt/lists/*

# install the PHP extensions we need (https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions)
RUN set -ex; \
    \
    savedAptMark="$(apt-mark showmanual)"; \
    \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        libavif-dev \
        libfreetype6-dev \
        libicu-dev \
        libjpeg-dev \
        libmagickwand-dev \
        libpng-dev \
        libwebp-dev \
        libzip-dev \
    ; \
    \
    docker-php-ext-configure gd \
        --with-avif \
        --with-freetype \
        --with-jpeg \
        --with-webp \
    ; \
    docker-php-ext-install -j "$(nproc)" \
        bcmath \
        exif \
        gd \
        intl \
        mysqli \
        zip \
        pdo_mysql \
    ; \
    # https://pecl.php.net/package/imagick
    pecl install imagick-3.8.1; \
    pecl install redis; \
    docker-php-ext-enable imagick redis; \
    rm -r /tmp/pear; \
    \
    # some misbehaving extensions end up outputting to stdout 🙈 (https://github.com/docker-library/wordpress/issues/669#issuecomment-993945967)
    out="$(php -r 'exit(0);')"; \
    [ -z "$out" ]; \
    err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \
    [ -z "$err" ]; \
    \
    extDir="$(php -r 'echo ini_get("extension_dir");')"; \
    [ -d "$extDir" ]; \
    # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
    apt-mark auto '.*' > /dev/null; \
    apt-mark manual $savedAptMark; \
    ldd "$extDir"/*.so \
        | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \
        | sort -u \
        | xargs -r dpkg-query --search \
        | cut -d: -f1 \
        | sort -u \
        | xargs -rt apt-mark manual; \
    \
    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
    rm -rf /var/lib/apt/lists/*; \
    \
    ! { ldd "$extDir"/*.so | grep 'not found'; }; \
    # check for output like "PHP Warning:  PHP Startup: Unable to load dynamic library 'foo' (tried: ...)
    err="$(php --version 3>&1 1>&2 2>&3)"; \
    [ -z "$err" ]

# set recommended PHP.ini settings
# see https://secure.php.net/manual/en/opcache.installation.php
# enabled by default in the php images: https://github.com/docker-library/php/pull/1587
RUN set -eux; \
    { \
        echo 'opcache.memory_consumption = 256'; \
        echo 'opcache.interned_strings_buffer = 16'; \
        echo 'opcache.max_accelerated_files = 20000'; \
        echo 'opcache.revalidate_freq = 2'; \
    } > "$PHP_INI_DIR/conf.d/opcache-recommended.ini"

RUN set -eux; \
    { \
        echo 'memory_limit = 512M'; \
        echo 'post_max_size = 64M'; \
        echo 'upload_max_filesize = 64M'; \
    } > "$PHP_INI_DIR/conf.d/wordpress-optimised.ini"

# https://wordpress.org/support/article/editing-wp-config-php/#configure-error-logging
RUN set -eux; \
    { \
        # https://www.php.net/manual/en/errorfunc.constants.php
        # https://github.com/docker-library/wordpress/issues/420#issuecomment-517839670
        echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \
        echo 'display_errors = Off'; \
        echo 'display_startup_errors = Off'; \
        echo 'log_errors = On'; \
        echo 'error_log = /dev/stderr'; \
        echo 'log_errors_max_len = 1024'; \
        echo 'ignore_repeated_errors = On'; \
        echo 'ignore_repeated_source = Off'; \
        echo 'html_errors = Off'; \
    } > "$PHP_INI_DIR/conf.d/error-logging.ini"

VOLUME /var/www/html

CMD ["php-fpm"]

如果这些文件都准备完毕、修改妥当,那现在可以先把 PHP 给编译了先:

docker compose build

编译完毕没有报错出现,即可上线所有服务测试了!

docker compose up -d

对于 sites 目录的所有者,在上传完网站源码后,需要修改所有者以确保正常运作:

chown -R 33:33 /data/web/sites

公交车司机终于在众人的指责中将座位让给了老太太