随着 Docker 和 Docker Compose 技术的日益成熟,越来越多的站长和开发者选择在 VPS 上使用容器化方案来部署 Web 应用。相比于传统的 LNMP 环境搭建方式,容器化部署具有环境隔离、快速迁移、版本控制、水平扩展等显著优势。本文将基于实战经验,从零开始详细讲解如何在 VPS 上使用 Docker + Docker Compose 构建一套高可用的 Web 应用栈,涵盖 Nginx 反向代理、PHP-FPM、MySQL、Redis、WordPress 等常见服务的容器化部署,并配合 Let’s Encrypt 自动续签 HTTPS 证书,真正做到开箱即用、维护无忧。
无论你是刚入门的小白还是有一定经验的老手,这篇文章都会提供大量可运行的代码示例和最佳实践,帮助你彻底掌握 VPS 容器化部署的精髓。

一、为什么要在 VPS 上使用 Docker 部署 Web 应用
在传统的 VPS 建站方案中,我们通常直接在宿主机上安装 Nginx、MySQL、PHP 等组件。这种方式虽然直观,但在实际运维中会面临几个难以回避的问题:
| 对比维度 | 传统 LNMP 部署 | Docker 容器化部署 |
|---|---|---|
| 环境一致性 | 依赖系统包管理器版本,迁移需重新配置 | 镜像构建一次,到处运行 |
| 依赖冲突 | 多个项目可能需要不同 PHP/MySQL 版本 | 每个容器独立版本,互不干扰 |
| 备份与恢复 | 散落在系统各目录,备份脚本需精确指定路径 | 通过 docker-compose.yml + 数据卷即可完整还原 |
| 资源隔离 | 一个服务崩了可能拖垮整个系统 | 容器级隔离,崩溃不影响其他服务 |
| 扩展能力 | 单机部署,扩展需要重新配置 | 天然支持多副本和负载均衡 |
对于一台 1核1G 甚至 512MB 内存的小鸡来说,Docker 的资源开销完全可以接受——Docker 引擎本身大约占用 50-100MB 内存,相比它带来的运维便利性,这点开销是值得的。
二、基础环境准备:安装 Docker 和 Docker Compose
在开始之前,我们先在 VPS 上安装 Docker 环境。以下操作基于 Ubuntu 22.04(Debian 系操作类似)。
2.1 一键安装 Docker
# 卸载旧版本
sudo apt remove docker docker-engine docker.io containerd runc
# 安装依赖并添加 Docker 官方源
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装 Docker 引擎
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 将当前用户加入 docker 组(避免每次 sudo)
sudo usermod -aG docker $USER
newgrp docker
# 验证安装
docker --version && docker compose version
2.2 系统调优:让 Docker 运行更稳定
安装完成后,对 Docker 的存储驱动和网络做一些基础调优,尤其对低配 VPS 至关重要:
# 配置 Docker 使用 overlay2 存储驱动(默认已是)
sudo tee /etc/docker/daemon.json <<'EOF'
{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"registry-mirrors": ["https://mirror.ccs.tencentyun.com"]
}
EOF
# 重启 Docker
sudo systemctl restart docker
# 确认配置生效
docker info | grep -i "storage driver"
日志限制非常重要——默认情况下 Docker 不限制容器日志文件大小,运行几周后日志文件可能吃掉你十几 GB 磁盘空间。上面的 max-size: 10m, max-file: 3 将每个容器的日志限制在 30MB 以内。
三、构建核心 Web 服务栈:Nginx + PHP-FPM + MySQL
这一节我们构建一个标准的 Web 服务栈。采用分层设计,让各个服务通过 Docker 内部网络通信。
3.1 项目目录结构
首先建立清晰的项目目录结构,这将极大方便后续的维护和备份:
mkdir -p ~/docker-web/{nginx/{conf.d,ssl,logs},php,www,db}
cd ~/docker-web
tree -L 2
预期的目录结构如下:
docker-web/
├── docker-compose.yml
├── nginx/
│ ├── conf.d/ # Nginx 站点配置文件
│ ├── ssl/ # SSL 证书目录
│ └── logs/ # Nginx 日志
├── php/
│ └── Dockerfile # 自定义 PHP 镜像
├── www/ # 网站源码根目录
└── db/ # MySQL 数据持久化目录
3.2 创建 docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:1.25-alpine
container_name: web-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./nginx/logs:/var/log/nginx
- ./www:/var/www/html
networks:
- webnet
depends_on:
- php
restart: unless-stopped
php:
build: ./php
container_name: web-php
volumes:
- ./www:/var/www/html
networks:
- webnet
environment:
- PHP_MEMORY_LIMIT=256M
- PHP_MAX_EXECUTION_TIME=300
- PHP_POST_MAX_SIZE=64M
- PHP_UPLOAD_MAX_FILESIZE=64M
restart: unless-stopped
mysql:
image: mysql:8.0
container_name: web-mysql
volumes:
- ./db:/var/lib/mysql
networks:
- webnet
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-wordpress}
MYSQL_USER: ${MYSQL_USER:-wpuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
command: --innodb_buffer_pool_size=256M --max_connections=100
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: web-redis
volumes:
- redis-data:/data
networks:
- webnet
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 128mb --maxmemory-policy allkeys-lru
restart: unless-stopped
networks:
webnet:
driver: bridge
volumes:
redis-data:
3.3 自定义 PHP Dockerfile
创建一个包含常用 PHP 扩展的自定义镜像,这对运行 WordPress 和大多数 PHP 应用是必需的:
cat > ~/docker-web/php/Dockerfile <<'EOF'
FROM php:8.2-fpm-alpine
# 安装系统依赖
RUN apk add --no-cache \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
libzip-dev \
zip \
unzip \
curl \
git
# 安装 PHP 扩展
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
gd \
pdo_mysql \
mysqli \
opcache \
zip \
bcmath \
exif
# 安装 Redis 扩展
RUN pecl install redis && docker-php-ext-enable redis
# 配置 OPcache
RUN { \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.interned_strings_buffer=8'; \
echo 'opcache.max_accelerated_files=10000'; \
echo 'opcache.revalidate_freq=2'; \
echo 'opcache.fast_shutdown=1'; \
echo 'opcache.enable_cli=1'; \
} > /usr/local/etc/php/conf.d/opcache.ini
# 安装 Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
EOF
四、创建 .env 环境变量文件
敏感信息(数据库密码、Redis 密码等)不应该硬编码在 docker-compose.yml 中。在同一目录下创建 .env 文件:
cat > ~/docker-web/.env <<'EOF'
MYSQL_ROOT_PASSWORD=YourStrongRootPassword123!
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
MYSQL_PASSWORD=YourStrongUserPassword456!
REDIS_PASSWORD=YourRedisPassword789!
EOF
# 设置 .env 文件权限,防止被其他用户读取
chmod 600 ~/docker-web/.env
五、配置 Nginx 反向代理与 SSL
5.1 Nginx 站点配置文件
创建一个通用的 Nginx 站点配置,支持 PHP 解析和 HTTPS:
cat > ~/docker-web/nginx/conf.d/default.conf <<'EOF'
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?|ttf|svg|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~ /\.ht {
deny all;
}
}
EOF
5.2 使用 acme.sh 自动获取 Let’s Encrypt 证书
不需要在容器内获取证书,在宿主机上使用 acme.sh,然后将证书挂载到 Nginx 容器即可:
# 安装 acme.sh
curl https://get.acme.sh | sh
# 设置默认 CA 为 Let's Encrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
# 申请证书(需要先确保 80 端口已指向你的 VPS)
~/.acme.sh/acme.sh --issue -d yourdomain.com -d www.yourdomain.com --standalone
# 将证书复制到 Nginx SSL 目录
~/.acme.sh/acme.sh --install-cert -d yourdomain.com \
--cert-file ~/docker-web/nginx/ssl/fullchain.pem \
--key-file ~/docker-web/nginx/ssl/privkey.pem \
--reloadcmd "docker exec web-nginx nginx -s reload"
acme.sh 会自动安装 cron 任务,每 60 天续期一次。配合上面的 --reloadcmd,续期后自动通知容器内的 Nginx 重新加载证书,整个过程完全自动化。
六、一键启动并部署 WordPress
所有配置文件就位后,启动整个服务栈:
cd ~/docker-web
# 首次构建 PHP 镜像并启动所有服务
docker compose up -d --build
# 查看运行状态
docker compose ps
# 查看启动日志
docker compose logs -f
# 下载 WordPress 并解压到 www 目录
cd www
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz --strip-components=1
rm latest.tar.gz
cd ..
# 设置正确的权限(PHP-FPM 需要 www-data 用户)
docker exec web-php chown -R www-data:www-data /var/www/html
启动完成后,访问 https://yourdomain.com 即可看到 WordPress 安装向导。数据库连接信息填写:
- 数据库主机: mysql(Docker 内部网络通过服务名解析)
- 数据库名: wordpress(与 .env 中 MYSQL_DATABASE 一致)
- 数据库用户: wpuser(与 .env 中 MYSQL_USER 一致)
- 数据库密码: 对应 .env 中的 MYSQL_PASSWORD
七、容器日常运维必备命令
掌握以下常用命令,日常维护不再犯难:
| 操作 | 命令 | 说明 |
|---|---|---|
| 查看运行中的容器 | docker compose ps |
显示所有容器状态 |
| 查看各容器日志 | docker compose logs -f |
跟踪实时日志输出 |
| 查看单个容器日志 | docker logs web-nginx --tail 100 |
只看 Nginx 的最后 100 行 |
| 重启单个服务 | docker compose restart nginx |
只重启 Nginx,不影响其他服务 |
| 进入容器内操作 | docker exec -it web-php bash |
登录 PHP 容器 Shell |
| 更新镜像并重建 | docker compose pull && docker compose up -d |
更新全部服务到最新版 |
| 停止并删除所有容器 | docker compose down |
数据卷中的数据保留 |
| 彻底清理 | docker compose down -v |
⚠️ 会删除数据卷中的数据 |
八、备份与迁移最佳实践
容器化方案的备份核心是:备份 docker-compose.yml、配置文件目录、数据库 dump 文件。相比传统 LNMP 环境需要从各个系统目录扒文件,这个流程简洁清晰得多:
#!/bin/bash
# backup.sh - 一键备份脚本
BACKUP_DIR="/root/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# 1. 备份数据库
docker exec web-mysql mysqldump -u root -p${MYSQL_ROOT_PASSWORD} \
--all-databases --single-transaction > $BACKUP_DIR/db_${DATE}.sql
# 2. 打包配置文件和网站数据
tar -czf $BACKUP_DIR/docker-web_${DATE}.tar.gz \
-C /root docker-web/.env \
-C /root docker-web/docker-compose.yml \
-C /root docker-web/nginx/conf.d \
-C /root docker-web/nginx/ssl \
-C /root docker-web/php/Dockerfile \
-C /root docker-web/www/wp-content
# 3. 同步到远程备份或对象存储(可选)
# rclone copy $BACKUP_DIR remote:backups/
# 4. 保留最近 7 天,删除更早的备份
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
echo "备份完成: $BACKUP_DIR/docker-web_${DATE}.tar.gz"
迁移到新 VPS 时,只需在新机器上安装 Docker,将备份的 tar.gz 解压,运行 docker compose up -d,再恢复数据库即可,全过程不超过 10 分钟。
九、性能优化与安全加固
9.1 容器资源限制
在一台低配 VPS 上运行多个容器时,强烈建议给每个容器分配资源上限,防止某个服务突发内存泄漏拖垮整个宿主机:
services:
mysql:
image: mysql:8.0
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
# ... 其余配置保持不变
9.2 网络安全最佳实践
- 对外只暴露 80/443:MySQL、Redis 等内部服务不应该绑定到宿主机端口,只通过 Docker 内部网络通信。前面 docker-compose.yml 的设计已经做到了这一点——没有向宿主机暴露 3306 或 6379 端口。
- 使用非 root 运行容器:在 Dockerfile 中可以通过
USER指令切换到普通用户运行,降低容器逃逸风险。PHP-FPM 的 Alpine 镜像默认以 www-data 运行,已经是一个好基础。 - 定期更新基础镜像:每个月运行一次
docker compose pull && docker compose up -d,确保使用最新的安全补丁。 - 启用 Docker 内容信任:
export DOCKER_CONTENT_TRUST=1让 Docker 只拉取经过签名的镜像。
9.3 Nginx 安全头配置
在 Nginx 配置中添加安全响应头,提升站点安全性得分:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
十、常见问题与排错指南
10.1 容器启动后立即退出
最常见的原因是端口冲突或配置文件语法错误。排查方法:
# 查看容器退出后的日志
docker compose logs nginx
docker compose logs php
# 检查端口是否被占用
sudo lsof -i :80
sudo lsof -i :443
# 测试 Nginx 配置是否正确(进入容器内)
docker exec web-nginx nginx -t
10.2 数据库连接失败
WordPress 安装时提示 “数据库连接错误”,通常的原因是:
- 数据库主机名错误:在 WordPress 中数据库主机填
mysql而不是localhost或127.0.0.1,因为这是 Docker 内部网络的服务名。 - MySQL 容器未就绪:PHP 容器启动时 MySQL 可能还没完成初始化。可以在 docker-compose.yml 中添加健康检查机制。
- 字符集问题:确保 MySQL 配置文件或启动参数中设置了
character-set-server=utf8mb4。
10.3 上传文件大小限制
如果 WordPress 后台无法上传大文件,需要同时修改三个地方:
- Nginx 层面:在 server 块中添加
client_max_body_size 64M; - PHP 层面:设置
upload_max_filesize和post_max_size(已在 Dockerfile 环境变量中设置) - WordPress 层面:在 wp-config.php 中添加
define('WP_MEMORY_LIMIT', '256M');
十一、进阶:用 Traefik 替代 Nginx 实现自动反向代理
如果你管理多个站点,手动维护 Nginx 配置文件会很繁琐。Traefik 是一个云原生反向代理,能够自动发现 Docker 容器并为其分配域名和 SSL 证书。只需在容器 label 中声明域名,Traefik 会自动处理路由和证书申请:
services:
traefik:
image: traefik:v3.0
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "./traefik/letsencrypt:/letsencrypt"
labels:
- "traefik.enable=true"
wordpress:
image: wordpress:6-php8.2-fpm-alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(\`blog.yourdomain.com\`)"
- "traefik.http.routers.wordpress.tls=true"
- "traefik.http.routers.wordpress.tls.certresolver=letsencrypt"
# ... 其他配置
此后每增加一个站点,只需在容器 label 中添加对应域名规则,Traefik 会自动完成证书申请和反向代理配置,真正实现”零接触”运维。
总结
本文从零开始详细介绍了如何在 VPS 上使用 Docker + Docker Compose 部署高可用的 Web 应用栈。从 Docker 安装、服务栈搭建、SSL 自动配置,到日常运维、备份迁移和安全加固,每一个环节都给出了可执行的代码和配置。
相较于传统的 LNMP 一键包方案,容器化部署虽然初期需要多花半小时搭建 docker-compose.yml,但在后续的维护、升级和迁移过程中节省的时间是数倍甚至数十倍的。特别是当你需要在同一台 VPS 上运行多个不同技术栈的站点时,Docker 的环境隔离优势更加明显。
如果你还在犹豫是否要切换到容器化方案,建议先花一个下午照着本文的步骤在测试环境中跑一遍,感受一下”docker compose up -d”一键启动整个应用的畅快感。一旦上手,你就会发现再也回不去传统的手动配置方式了。
汤不热吧