Nginx 加 MySQL 是服务器上最经典的组合:前者做 Web 服务或反向代理,后者存业务数据。用 Docker 部署这套组合,能省去手工编译安装、依赖冲突、升级麻烦等一堆问题,但也带来几个必须处理好的细节——配置怎么挂、数据怎么持久化、初始化脚本怎么执行、字符集和认证插件怎么配。这篇文章用一条完整的实操路线,从目录规划、用 docker run 分别部署,到用 Compose 一次性拉起两个服务,把每个关键配置讲清楚,并列出容易踩的坑和连接测试方法。照着做能在十几分钟内跑起一套可用的 Nginx + MySQL。
一、准备工作:目录规划与镜像拉取
先把宿主机上的目录规划好。Docker 部署最忌讳"文件散落各处、下次不知道怎么恢复",一个清晰的目录结构能省很多事。推荐把配置、数据、初始化脚本分目录管理:
# 建议的目录结构(示例,可按需调整)
mkdir -p /opt/stack/nginx/conf.d
mkdir -p /opt/stack/nginx/html
mkdir -p /opt/stack/mysql/initdb
mkdir -p /opt/stack/mysql/conf.d
cd /opt/stack
# 拉取镜像(指定具体版本,避免 latest 漂移)
docker pull nginx:1.25-alpine
docker pull mysql:8.0
这里强调一个实践原则:镜像一定要用具体的版本标签(如 nginx:1.25-alpine、mysql:8.0),不要用 latest。latest 会随上游更新而变,哪天拉了个新版本导致行为不一致,排查起来很痛苦。锁版本是可复现部署的基础。
二、docker run 方式部署 Nginx
Nginx 的部署要点是"配置和静态文件挂进来、端口映射出来"。
docker run -d --name nginx \
--restart unless-stopped \
-p 80:80 \
-v /opt/stack/nginx/conf.d:/etc/nginx/conf.d:ro \
-v /opt/stack/nginx/html:/usr/share/nginx/html:ro \
-v /opt/stack/nginx/logs:/var/log/nginx \
nginx:1.25-alpine
参数逐条说明:-d 后台运行;--restart unless-stopped 让容器在宿主重启或异常退出后自动拉起;-p 80:80 把宿主 80 端口映射到容器 80;conf.d 和 html 以只读方式挂载,避免容器误改宿主的配置和页面;日志目录单独挂出来,方便宿主上用工具分析,也避免日志写满容器可写层。注意这里用了 -v 便于演示,生产上更推荐 --mount(会显式报错而不是静默创建目录,见数据卷那一篇)。
配置一个站点,新建 /opt/stack/nginx/conf.d/default.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# 健康检查端点,方便容器编排探活
location = /health {
access_log off;
return 200 'ok';
add_header Content-Type text/plain;
}
}
改完配置后重载,不用重建容器:
# 校验配置语法(重要,语法错会让 nginx 起不来)
docker exec nginx nginx -t
# 平滑重载
docker exec nginx nginx -s reload
# 或直接重启容器
docker restart nginx
访问测试:
curl -I http://127.0.0.1/
如果返回 200,说明 Nginx 已经正常提供页面;返回 404 通常是 html 目录里还没有 index.html,放一个测试页进去即可。也可以带上 -H "Host: 你的域名" 来验证基于域名的虚拟主机是否命中正确的 server 块。一个小提示:Nginx 容器的日志分 access.log 和 error.log,排查 404、502 这类问题时优先看 error.log,curl 拿到的状态码只是结果,真正的原因在错误日志里。
三、docker run 方式部署 MySQL
MySQL 的部署要点多一些,因为涉及密码、数据卷、字符集。
docker run -d --name mysql \
--restart unless-stopped \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD='请换成强密码' \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD='请换成强密码' \
-e TZ=Asia/Shanghai \
-v mysql-data:/var/lib/mysql \
-v /opt/stack/mysql/initdb:/docker-entrypoint-initdb.d:ro \
-v /opt/stack/mysql/conf.d:/etc/mysql/conf.d:ro \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci \
mysql:8.0
上面有两个写法需要说明。一是 -e 传的环境变量:MYSQL_ROOT_PASSWORD 是必须的,MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD 会额外建一个库和一个普通用户;TZ 设置时区,避免时间显示错误。二是最后的 --character-set-server=utf8mb4 这类参数,必须放在镜像名 mysql:8.0 之后——它们在镜像名后面的都是传给容器内 mysqld 的参数,写错位置会被当成镜像名导致启动失败。为了避免这种位置错误,更规范的做法是把字符集写进配置文件:
# /opt/stack/mysql/conf.d/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-time-zone = '+08:00'
[client]
default-character-set = utf8mb4
数据卷 mysql-data 是命名卷,MySQL 的数据目录 /var/lib/mysql 挂到它上面,容器删了数据不丢。/docker-entrypoint-initdb.d 是 MySQL 官方镜像的特殊目录:容器首次初始化(数据目录为空)时,会按文件名顺序执行里面的 .sh、.sql、.sql.gz。注意"只在数据目录为空时执行"——已经初始化过的库,重启容器不会再跑这些脚本,想重新初始化得先清空数据卷。
准备好初始化脚本:
-- /opt/stack/mysql/initdb/01-schema.sql
CREATE DATABASE IF NOT EXISTS appdb
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE appdb;
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users (name) VALUES ('测试用户');
-- /opt/stack/mysql/initdb/02-grants.sql
-- 注意 8.0 默认认证插件是 caching_sha2_password,老客户端可能连不上
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY '请换成强密码';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
关于认证插件:MySQL 8.0 默认用 caching_sha2_password,一些老版本的客户端驱动或老工具不支持,会报 Authentication plugin 'caching_sha2_password' cannot be loaded。上面示例显式指定 mysql_native_password 来兼容老客户端。如果客户端够新,其实不必改,保持默认更安全——这是取舍,按你的客户端版本定。
启动后查看初始化和运行状态:
docker logs mysql 2>&1 | tail -30
# 看到 "ready for connections" 表示启动完成
docker exec -it mysql mysql -uroot -p
在 MySQL 里确认字符集:
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
四、Compose 方式:一次拉起 Nginx + MySQL
如果两台服务要一起管,用 Compose 更清爽。在 /opt/stack/compose.yaml:
services:
nginx:
image: nginx:1.25-alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/html:/usr/share/nginx/html:ro
- ./nginx/logs:/var/log/nginx
networks:
- app-net
depends_on:
mysql:
condition: service_healthy
mysql:
image: mysql:8.0
container_name: mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
ports:
- "127.0.0.1:3306:3306" # 只绑定本机,避免数据库直接暴露公网
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/initdb:/docker-entrypoint-initdb.d:ro
- ./mysql/conf.d:/etc/mysql/conf.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
networks:
- app-net
volumes:
mysql-data:
networks:
app-net:
driver: bridge
配套 .env:
# /opt/stack/.env —— 务必加入 .gitignore,不要提交
MYSQL_ROOT_PASSWORD=换成强密码
MYSQL_PASSWORD=换成强密码
启动与验证:
cd /opt/stack
docker compose up -d
docker compose ps
docker compose logs -f --tail=50
这里有个关键配置:端口写成 "127.0.0.1:3306:3306",表示只监听宿主的回环地址。这样即使服务器有公网 IP,MySQL 也不会直接暴露在公网,必须通过本机或经跳板访问,能挡掉大量自动扫描的暴力破解。这是生产环境的基本安全习惯。
五、连接测试
部署完必须验证"应用能连上数据库",而不只是"容器在跑"。
# 从宿主机测试 MySQL(需要宿主装了客户端)
mysql -h127.0.0.1 -P3306 -uappuser -p appdb -e 'SELECT COUNT(*) FROM users;'
# 从 nginx 容器里测试到 mysql 的连通性(Compose 网络内用服务名)
docker exec nginx sh -c 'getent hosts mysql'
# 若 nginx 镜像里没有 mysql 客户端,用网络工具测试端口可达
docker exec nginx sh -c 'nc -zv mysql 3306 || echo "nc 不可用,改用应用层测试"'
# 用一次性容器测试(推荐,不依赖宿主装客户端)
docker run --rm --network stack_app-net mysql:8.0 \
mysql -hmysql -uappuser -p'你的密码' appdb -e 'SELECT COUNT(*) FROM users;'
最后一条命令的思路很实用:用同样在 app-net 网络里的临时 MySQL 客户端容器,以"应用视角"去连服务名 mysql,验证 DNS 解析和网络连通性都正常。注意网络名是 Compose 自动生成的 <项目名>_<网络名> 形式,可用 docker network ls 确认实际名称。
六、常见坑与注意事项
坑一:字符集没配好导致中文乱码。表现是插入中文变成问号或乱码。要在服务端(character-set-server)和客户端连接串里都用 utf8mb4,不要用 utf8(utf8 在 MySQL 里是 3 字节,存不了 emoji 和部分字符)。
坑二:认证插件不兼容。旧客户端连不上 MySQL 8.0 的 caching_sha2_password。要么升级客户端,要么用户用 mysql_native_password 创建。
坑三:数据卷里已有数据,初始化脚本不执行。/docker-entrypoint-initdb.d 只在数据目录为空时生效。想重跑初始化,先删卷(会丢数据,务必确认)或换一个空卷。
坑四:MySQL 启动比 Nginx 慢,Nginx 先起来时上游连不上数据库。用 healthcheck + depends_on: condition: service_healthy,或让应用做连接重试。
坑五:把 MySQL 端口直接暴露公网。除了 127.0.0.1 绑定,还应配合防火墙只放行必要端口。风险提示:调整防火墙和端口前务必确认不会切断自己的 SSH 连接,改动前先备份规则。
坑六:挂载空目录遮住镜像原有配置。Nginx 官方镜像的 /etc/nginx/conf.d 里有一个默认配置,如果你挂一个空目录进去,它会消失;如果你自己写了 default.conf 则没问题。MySQL 的 /etc/mysql/conf.d 同理。确保挂载的目录里有你需要的内容。
# 排查容器问题常用
docker logs nginx --tail=50
docker logs mysql --tail=50
docker exec -it mysql mysql -uroot -p -e 'SHOW VARIABLES LIKE "%character%";'
docker inspect mysql --format '{{json .State.Health.Status}}'
常见问题
Q:为什么我的 MySQL 容器起来又退出?
先看 docker logs mysql。最常见是 MYSQL_ROOT_PASSWORD 没设(官方镜像要求必须设),或数据目录权限问题,或字符集参数位置写错被当成镜像名。日志里会有明确原因。
Q:数据存在哪里,怎么备份?
数据在命名卷 mysql-data 里。备份优先用 mysqldump 做逻辑备份,或停库后用临时容器打包卷。只备份容器本身没意义,必须备份卷或导出 SQL。
Q:改了 Nginx 配置要重建容器吗?
不需要。配置是挂载进去的,改完执行 docker exec nginx nginx -t 校验再用 nginx -s reload 平滑重载即可。只有改了镜像或挂载关系才需要重建。
Q:MySQL 端口能不能不暴露到宿主?
可以,且推荐。如果只有同网络的容器访问,连 ports 都不用映射,容器间直接用服务名互访。需要宿主访问时,绑定到 127.0.0.1 更安全。
Q:怎么初始化更多数据库或建更多用户?
在 /docker-entrypoint-initdb.d 里按顺序放 .sql 脚本(文件名前缀数字控制顺序)。记住只在首次初始化时执行一次,之后要手工执行或换空卷。
Q:Nginx 和 MySQL 能放在同一台服务器上吗?
能,这也是中小企业常见的做法。但要注意资源分配:给两个容器分别设置内存上限,避免 MySQL 缓存吃满内存拖垮系统;同时监控磁盘,数据库文件和 Nginx 日志都在长。如果业务增长,再考虑把数据库独立到另一台机器。
总结
用 Docker 部署 Nginx + MySQL 的核心是三件事:配置挂载、数据持久化、初始化脚本。Nginx 挂 conf.d 和 html,MySQL 挂数据卷和 initdb,各自分工明确。
镜像锁具体版本(不要 latest),容器加 --restart unless-stopped,数据库端口绑定 127.0.0.1,这些是生产环境的基本功。
字符集统一用 utf8mb4,MySQL 8.0 注意 caching_sha2_password 与老客户端的兼容;初始化脚本只在数据目录为空时执行,这一点务必记住。
Compose 适合管理这套多容器组合,用 healthcheck + depends_on 解决 MySQL 启动慢导致的依赖未就绪问题,比手工 sleep 可靠。
部署完要用"应用视角"测试连接(用同网络的临时客户端容器连服务名),而不只是看容器是否 running;容器在跑不等于服务可用。
企业QQ咨询




