1. 为什么“装服务”才是Docker真正的主战场
很多人学Docker,卡在第一步——装完Docker本身,跑了个hello-world,然后就不知道干什么了。镜像也拉了,容器也跑了,命令也敲了,但回到实际工作里,还是不知道怎么用Docker去跑一个MySQL、跑一个Redis、跑一个Nginx。这其实是绝大多数人学Docker的真实状态:工具会用了,但场景接不上。
这篇内容就是来解决这个问题的。我前后在几台不同的机器上装过Docker,也在不同的项目里用Docker跑过各种常用服务,踩过的坑不算少。今天把Docker安装常用服务这件事从头到尾捋一遍,包括每个服务为什么这么装、参数为什么这么配、装完之后怎么验证、出问题怎么排查。不管你是刚学完Docker基础命令的新手,还是已经用过一段时间但没系统整理过的老手,这篇内容应该都能让你有所收获。
所谓“常用服务”,在我的实际工作里,出现频率最高的无非这几类:数据库类(MySQL、Redis、PostgreSQL、MongoDB)、Web服务类(Nginx、Tomcat)、消息队列类(RabbitMQ、Kafka)、以及一些辅助工具(比如Portainer做可视化管理)。这些服务如果用传统方式安装,每个都要配源、装依赖、改配置、设开机自启,一套下来少说半小时,多了可能折腾半天。用Docker装,快的话一条命令就起来了,而且环境隔离干净,删了不留痕迹。
但“一条命令起来”只是表面,真正要把它用好,你得理解端口映射的逻辑、数据持久化的方式、配置文件怎么挂载、容器之间怎么通信、以及生产环境要注意什么。这些才是这篇内容的核心。
提示:这篇内容假设你已经装好了Docker引擎,基本的
docker pull、docker run、docker ps命令会用。如果还没到这一步,建议先把Docker本身的安装和基础命令过一遍再来看。
接下来我会按照“整体思路 → 核心细节 → 实操过程 → 问题排查”的顺序展开,每个服务都会给出可直接复制的命令和参数解释,同时穿插我在实际使用中总结的经验和教训。
2. 整体设计思路:装服务之前先想清楚这几件事
2.1 容器化装服务的核心逻辑
传统装服务的方式,是在宿主机上直接安装软件包,配置文件散落在/etc下,数据存在/var/lib下,日志在/var/log下,卸载的时候经常卸不干净。Docker的方式是把每个服务封装成一个独立的容器,容器里有自己的文件系统、自己的网络、自己的进程空间。
这样做的好处很直接:环境隔离、版本可控、迁移方便、清理彻底。但代价是,你得额外考虑几件事——数据怎么持久化、网络怎么暴露、配置怎么注入、容器挂了怎么恢复。这四件事想清楚了,装任何服务都是同一套逻辑。
我一般把装一个服务的思考过程拆成四步:
- 这个服务需要暴露哪些端口?比如MySQL默认3306,Redis默认6379,Nginx默认80和443。
- 这个服务的数据存在哪里?数据库类的必须做数据卷映射,否则容器一删数据全没。
- 这个服务有没有配置文件需要自定义?有的话就挂载进去,没有就用默认的。
- 这个服务需不需要开机自启?生产环境一般都要,用
--restart参数控制。
把这四个问题回答清楚,命令基本就出来了。
2.2 镜像选择:官方镜像 vs 第三方镜像
拉镜像的时候,我强烈建议优先用官方镜像。官方镜像的命名格式一般是服务名:标签,比如mysql:8.0、redis:7.2、nginx:latest。第三方镜像虽然有时候体积更小或者预装了一些工具,但安全性和维护频率没法保证。
标签的选择也有讲究。不要无脑用latest,这是我在生产环境踩过的最大的坑之一。latest意味着你每次拉到的可能都是不同版本,今天跑得好好的,明天重新部署可能就起不来了。正确的做法是锁定具体版本号,比如mysql:8.0.35、redis:7.2.4。这样无论什么时候重新拉,拿到的都是同一个版本。
当然,开发环境用latest图方便可以理解,但只要是稍微正式一点的环境,版本号必须锁死。
2.3 网络模式的选择
Docker默认有三种网络模式:bridge、host、none。装常用服务的时候,绝大多数情况用默认的bridge就够了。bridge模式下,容器有自己的独立网络,通过端口映射和宿主机通信,容器之间可以通过自定义网络互相访问。
host模式让容器直接使用宿主机的网络栈,性能更好,但端口会直接占用宿主机的,容易冲突。我一般只在性能敏感的场景下用host,比如高并发的Nginx。
还有一种做法是创建自定义bridge网络,把相关的服务都放到同一个网络里,这样容器之间可以直接用容器名互相访问,不用记IP。这个在多服务协作的场景下非常有用,后面讲具体服务的时候会演示。
2.4 数据持久化的三种方式
数据持久化是容器化装服务最容易出问题的地方。Docker提供了三种方式:
| 方式 | 命令示例 | 适用场景 | 特点 |
|---|---|---|---|
| 绑定挂载 | -v /host/path:/container/path | 配置文件、需要直接访问的数据 | 宿主机路径明确,方便备份 |
| 数据卷 | -v volume_name:/container/path | 数据库数据、日志 | Docker管理,跨容器共享方便 |
| 临时文件系统 | --tmpfs /container/path | 缓存、临时数据 | 不落盘,容器停止即消失 |
我的经验是:配置文件用绑定挂载,数据用数据卷。配置文件放宿主机上方便随时改,数据用数据卷让Docker管理更省心。当然也有人全部用绑定挂载,这样备份的时候直接打包目录就行,看个人习惯。
注意:绑定挂载的时候,宿主机路径必须用绝对路径,而且如果宿主机目录不存在,Docker会自动创建一个空目录,这有时候会导致权限问题。建议先手动创建好目录再挂载。
3. 核心细节解析:每个服务的安装要点与参数逻辑
3.1 MySQL的安装要点
MySQL是容器化装服务里最典型也最容易出问题的一个。核心参数有这么几个:
-p 3306:3306:端口映射,前面是宿主机端口,后面是容器端口。如果宿主机3306被占了,可以改成-p 3307:3306。-e MYSQL_ROOT_PASSWORD=your_password:设置root密码,这个必须设,不设容器起不来。-v /data/mysql:/var/lib/mysql:数据目录映射,这是重中之重,不映射的话容器一删数据全丢。--restart=always:开机自启,生产环境必加。
还有一个容易被忽略的点:字符集。默认的MySQL容器字符集是latin1,中文会乱码。需要在启动命令里加上--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci,或者在配置文件里设置。
我一般会先创建一个配置文件my.cnf,把字符集、大小写敏感、最大连接数这些参数都写进去,然后挂载到容器的/etc/mysql/conf.d/目录下。这样比在命令行里传一堆参数要清晰得多。
3.2 Redis的安装要点
Redis的安装相对简单,但有几个点要注意:
- 持久化配置:Redis默认是RDB持久化,如果对数据安全性要求高,建议开启AOF。可以在配置文件里设
appendonly yes。 - 密码设置:默认Redis没有密码,这是很危险的。启动时加
--requirepass your_password,或者在配置文件里设。 - 内存限制:容器环境下,Redis可能不知道自己的内存上限,建议通过
--maxmemory参数显式设置,并配置淘汰策略。
Redis的配置文件我一般会挂载redis.conf到/usr/local/etc/redis/redis.conf,然后启动命令里指定用这个配置文件。
3.3 Nginx的安装要点
Nginx容器化最核心的是配置文件的挂载和静态文件的挂载:
-v /data/nginx/conf.d:/etc/nginx/conf.d:把站点配置挂载进去。-v /data/nginx/html:/usr/share/nginx/html:把静态文件挂载进去。-v /data/nginx/logs:/var/log/nginx:日志挂载出来方便查看。
Nginx的配置文件修改后,不需要重启容器,执行docker exec nginx nginx -s reload就可以热加载。这个技巧在实际运维中非常实用。
3.4 容器间通信的设计
当你有多个服务需要互相访问的时候,比如Web服务要连MySQL,就涉及到容器间通信。最简单的做法是用自定义网络:
docker network create myapp-network然后启动每个容器的时候都加上--network myapp-network。这样容器之间可以直接用容器名作为主机名互相访问,比如Web服务的配置里数据库地址直接写mysql就行,不用写IP。
这个设计的好处是,IP变了也不影响,因为Docker内置了DNS解析。而且把不相关的服务放在不同网络里,还能起到隔离作用。
3.5 资源限制的必要性
容器默认可以使用宿主机的全部资源,这在多服务共存的环境下是危险的。一个服务内存泄漏可能把整台机器拖垮。所以启动容器的时候建议加上资源限制:
--memory=512m --cpus=1.5--memory限制内存,--cpus限制CPU核数。这两个参数在共享环境下尤其重要。我见过太多次因为没限制资源,一个容器把宿主机内存吃满导致所有服务不可用的情况。
4. 实操过程:从零开始装一套常用服务
4.1 环境准备与目录规划
先规划好目录结构,我一般会在/data下按服务名建目录:
mkdir -p /data/mysql/{data,conf,logs} mkdir -p /data/redis/{data,conf} mkdir -p /data/nginx/{conf,html,logs} mkdir -p /data/rabbitmq/{data,logs}这样每个服务的配置、数据、日志都分开放,备份的时候按目录打包就行,非常清晰。
然后创建一个自定义网络:
docker network create app-net后面所有服务都加入这个网络。
4.2 MySQL 8.0 安装实操
先写配置文件/data/mysql/conf/my.cnf:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-authentication-plugin=mysql_native_password max_connections=500 innodb_buffer_pool_size=256M lower_case_table_names=1这里解释几个关键参数:character-set-server设字符集,default-authentication-plugin设认证插件(MySQL 8默认的caching_sha2_password有些老客户端连不上,改成mysql_native_password兼容性更好),lower_case_table_names=1让表名不区分大小写(Linux下默认区分,容易踩坑)。
启动命令:
docker run -d \ --name mysql \ --network app-net \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root@123456 \ -e TZ=Asia/Shanghai \ -v /data/mysql/data:/var/lib/mysql \ -v /data/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /data/mysql/logs:/var/log/mysql \ --restart=always \ --memory=1g \ mysql:8.0.35启动后等十几秒,用docker logs mysql看日志,出现ready for connections就说明起来了。然后进容器验证:
docker exec -it mysql mysql -uroot -pRoot@123456能进去就说明安装成功。
实操心得:MySQL容器第一次启动会比较慢,因为要初始化数据目录。如果日志里一直卡在初始化阶段,大概率是数据目录权限问题。可以进容器看看
/var/lib/mysql的属主是不是mysql用户,不是的话需要chown一下。
4.3 Redis 7.2 安装实操
写配置文件/data/redis/conf/redis.conf:
bind 0.0.0.0 protected-mode yes port 6379 requirepass Redis@123456 appendonly yes appendfsync everysec maxmemory 512mb maxmemory-policy allkeys-lru dir /data关键参数说明:bind 0.0.0.0让Redis监听所有网卡(容器内必须这样,否则外部连不上),requirepass设密码,appendonly yes开启AOF持久化,maxmemory和maxmemory-policy控制内存使用和淘汰策略。
启动命令:
docker run -d \ --name redis \ --network app-net \ -p 6379:6379 \ -v /data/redis/data:/data \ -v /data/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf \ --restart=always \ --memory=768m \ redis:7.2.4 redis-server /usr/local/etc/redis/redis.conf注意最后要指定用哪个配置文件启动,否则容器会用默认配置,你挂载的配置文件就不生效了。
验证:
docker exec -it redis redis-cli -a Redis@123456 ping返回PONG就成功了。
4.4 Nginx 安装实操
Nginx的配置我一般分两个文件:主配置nginx.conf和站点配置default.conf。主配置用默认的就行,站点配置放/data/nginx/conf/conf.d/default.conf:
server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ =404; } location /api/ { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }启动命令:
docker run -d \ --name nginx \ --network app-net \ -p 80:80 \ -p 443:443 \ -v /data/nginx/conf/conf.d:/etc/nginx/conf.d \ -v /data/nginx/html:/usr/share/nginx/html \ -v /data/nginx/logs:/var/log/nginx \ --restart=always \ nginx:1.25.3改完配置后热加载:
docker exec nginx nginx -t && docker exec nginx nginx -s reload先测试配置语法,没问题再reload,这个习惯能避免很多线上事故。
4.5 RabbitMQ 安装实操
RabbitMQ带管理界面的镜像用rabbitmq:3.12-management:
docker run -d \ --name rabbitmq \ --network app-net \ -p 5672:5672 \ -p 15672:15672 \ -e RABBITMQ_DEFAULT_USER=admin \ -e RABBITMQ_DEFAULT_PASS=Admin@123456 \ -v /data/rabbitmq/data:/var/lib/rabbitmq \ -v /data/rabbitmq/logs:/var/log/rabbitmq \ --restart=always \ --memory=512m \ rabbitmq:3.12-management5672是AMQP协议端口,15672是管理界面端口。启动后浏览器访问http://宿主机IP:15672,用设置的账号密码登录就能看到管理界面。
4.6 用Docker Compose统一管理
一个个docker run敲命令太累,而且不好维护。实际项目里我推荐用Docker Compose把所有服务写在一个docker-compose.yml里:
version: '3.8' services: mysql: image: mysql:8.0.35 container_name: mysql restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: Root@123456 TZ: Asia/Shanghai volumes: - /data/mysql/data:/var/lib/mysql - /data/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf networks: - app-net deploy: resources: limits: memory: 1g redis: image: redis:7.2.4 container_name: redis restart: always ports: - "6379:6379" volumes: - /data/redis/data:/data - /data/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf networks: - app-net networks: app-net: external: true然后docker compose up -d一键启动所有服务,docker compose down一键停止。版本管理也方便,配置文件直接进Git仓库。
实操心得:Compose文件里的
version字段现在其实可以省略了,新版本的Docker Compose会忽略它。但为了兼容老版本,加上也没坏处。另外deploy.resources.limits在非Swarm模式下部分版本可能不生效,如果发现限制没起作用,还是用docker run的--memory参数更可靠。
5. 常见问题与排查技巧实录
5.1 容器启动后立刻退出
这是最常见的问题。排查步骤:
docker ps -a看容器状态,如果是Exited,记下退出码。docker logs 容器名看日志,大部分问题日志里都有线索。- 常见原因:配置文件语法错误、端口被占用、数据目录权限不对、环境变量没设。
退出码1一般是应用报错,137是被OOM Killer杀了(内存不够),139是段错误。记住这几个退出码,排查起来快很多。
5.2 端口被占用
报错信息一般是bind: address already in use。解决方法:
# 查看端口被谁占了 netstat -tlnp | grep 3306 # 或者 ss -tlnp | grep 3306找到进程后要么停掉它,要么改Docker的端口映射,比如-p 3307:3306。
5.3 数据目录权限问题
MySQL和RabbitMQ这类服务对数据目录权限很敏感。如果日志里出现Permission denied,进容器看看目录属主:
docker exec -it mysql ls -la /var/lib/mysql如果属主不是mysql用户,在宿主机上执行:
chown -R 999:999 /data/mysql/data999是MySQL容器里mysql用户的UID。不同镜像UID可能不同,用docker exec mysql id mysql可以查。
5.4 容器间无法通信
如果Web服务连不上MySQL,先确认它们在同一个网络里:
docker network inspect app-net看看两个容器是不是都在Containers列表里。然后进Web容器测试:
docker exec -it web ping mysql能ping通说明网络没问题,连不上就是应用配置的问题。ping不通就检查网络配置。
5.5 中文乱码
MySQL中文乱码基本就是字符集没设对。进MySQL执行:
SHOW VARIABLES LIKE 'character%';如果看到latin1,说明配置文件没生效。检查挂载路径对不对,配置文件有没有被正确加载。MySQL 8的配置文件加载顺序是/etc/my.cnf→/etc/mysql/my.cnf→/etc/mysql/conf.d/*.cnf,挂载到conf.d目录下一般没问题。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方法 |
|---|---|---|---|
| 容器启动即退出 | 配置错误/端口占用/权限 | docker logs 容器名 | 根据日志修复 |
| 外部连不上服务 | 端口没映射/防火墙 | docker port 容器名 | 检查-p参数和防火墙 |
| 数据丢失 | 没做数据卷映射 | docker inspect 容器名 | 重新挂载数据卷 |
| 中文乱码 | 字符集不对 | SHOW VARIABLES LIKE 'character%' | 配置文件设utf8mb4 |
| 内存溢出 | 没限制内存 | docker stats | 加--memory限制 |
| 容器间不通 | 不在同一网络 | docker network inspect | 加入同一自定义网络 |
| 配置文件不生效 | 挂载路径错误 | docker exec 容器名 cat 配置路径 | 检查挂载路径 |
5.7 几个我踩过的坑
坑一:MySQL 8的认证插件。MySQL 8默认用caching_sha2_password,一些老版本的客户端(比如某些语言的旧驱动)连不上,报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法就是在配置文件里加default-authentication-plugin=mysql_native_password。
坑二:Redis的protected-mode。如果配置文件里bind设的是127.0.0.1,外部就连不上。容器里必须设bind 0.0.0.0,但这样又要注意安全,所以密码必须设强一点。
坑三:Nginx的proxy_pass末尾斜杠。proxy_pass http://backend:8080/和proxy_pass http://backend:8080是不一样的,前者会替换掉location匹配的路径,后者会保留。这个细节不注意,接口路径就会404。
坑四:数据卷的备份。用数据卷的时候,备份不能直接复制宿主机目录(因为数据卷实际存在/var/lib/docker/volumes/下),要用docker run --rm -v 卷名:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .这种方式。绑定挂载就没这个问题,直接打包目录就行。这也是我后来更倾向于用绑定挂载的原因之一。
坑五:时区问题。容器默认时区是UTC,日志时间比北京时间少8小时。启动时加-e TZ=Asia/Shanghai,或者在配置文件里设时区。这个坑不致命但很烦人,排查日志的时候时间对不上很影响效率。
5.8 生产环境的额外注意事项
开发环境怎么方便怎么来,生产环境就得多考虑几层:
- 日志收集:容器日志默认存在
/var/lib/docker/containers/下,时间长了会占满磁盘。建议配置日志驱动,把日志转发到统一的地方,或者至少设置--log-opt max-size=100m --log-opt max-file=3做轮转。 - 健康检查:加
--health-cmd参数,让Docker定期检查服务是否正常,异常时可以自动重启。 - 镜像更新:不要用
latest,用具体版本号,更新的时候先拉新镜像、起新容器、验证没问题再切流量。 - 安全加固:容器里不要用root用户跑服务,加
--user参数指定非root用户。敏感信息用Docker Secret或者环境变量文件,不要直接写在命令行里(命令行会被docker inspect看到)。
这套东西全部配下来,一台机器上跑五六个常用服务,资源占用可控,管理也清晰。后面如果要迁移,把/data目录和docker-compose.yml拷过去,docker compose up -d就完事了,比传统方式省心太多。
我个人在实际操作中的体会是,Docker装服务这件事,命令本身不难,难的是把每个参数背后的逻辑想清楚。端口为什么这么映射、数据为什么这么挂、网络为什么这么配,这些想明白了,装任何服务都是同一套思路。另外就是一定要做数据持久化,我见过太多人容器删了数据没了才后悔。最后,生产环境能用Compose就用Compose,别手敲docker run,命令长了容易漏参数,而且不好维护。