<!DOCTYPE html>
Docker 容器化部署实战:从安装配置到 docker-compose 编排与排错全指南
在 VPS 上部署应用,最常见的痛点是环境不一致:本地能跑、服务器上跑不起来;升级一次依赖把整个系统搞崩;一台机器上多个服务互相抢占端口和依赖。Docker 用"镜像 + 容器"的方式把应用和它的运行环境打包成标准交付物,彻底解决了这些问题。本文从零开始,带你在一台全新 VPS 上完成 Docker 安装、镜像管理、容器运行、Dockerfile 构建、docker-compose 多服务编排,最后给出高频故障的排错手册。
一、Docker 核心概念:镜像、容器与仓库
动手之前先理清三个概念,后面所有命令都围绕它们展开:
- 镜像(Image):只读的应用模板,包含操作系统基础层、运行环境和代码。可以理解为"安装包"。同一镜像可创建任意多个容器。
- 容器(Container):镜像运行后的实例,是可写、可启动/停止的进程沙箱。所有容器共享宿主内核,因此启动毫秒级、资源占用极小。
- 仓库(Registry):集中存放镜像的地方,默认是 Docker Hub。国内访问 Docker Hub 经常超时,需要配置镜像加速器(下文会讲)。
理解它们的关系可以用一句话概括:镜像 = 类,容器 = 对象。写代码前先确认宿主内核版本,因为容器共享内核,低版本内核上跑高版本内核要求的镜像(如某些较新的 glibc 应用)会报错。
uname -r
# 建议内核 4.18+,Docker 20.10 以上版本要求 Linux 内核 3.10+
二、在 VPS 上一键安装 Docker 与 Docker Compose
官方提供了一键安装脚本,会自动配置 Docker 官方源并启动服务。如果服务器在国内,安装前建议先换成国内镜像源,避免安装过程超时。
# 1. 使用官方脚本安装 Docker(含 Docker Engine + CLI + containerd)
curl -fsSL https://get.docker.com | bash -s docker
# 2. 将当前用户加入 docker 组,免 sudo 执行(重新登录生效)
sudo usermod -aG docker $USER
# 3. 启动服务并设置开机自启
sudo systemctl enable --now docker
# 4. 验证安装
docker version
docker run --rm hello-world
Compose 插件安装:新版 Docker 的 compose 已内置为插件,无需单独装。验证方式如下。
docker compose version
# Docker Compose version v2.x.x
排错一:docker run 拉镜像超时。这是国内 VPS 最常见的问题,配置镜像加速器即可解决。编辑 /etc/docker/daemon.json:
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"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
上面的 log-opts 顺手把容器日志做了滚动限制,防止容器日志无限增长把磁盘写满——这是后面排错时反复出现的坑。
排错二:安装后 docker 命令报 permission denied。要么退出重新登录使 docker 组生效,要么临时用 sudo docker。不要随意 chmod 777 /var/run/docker.sock,这会带来严重安全风险。
三、镜像管理:拉取、查看与清理
镜像管理是日常最高频操作。常用命令如下:
# 拉取镜像,:alpine 表示极小体积的 Alpine Linux 版本
docker pull nginx:alpine
# 列出本地镜像
docker images
# 查看镜像详细信息(含架构、层数、环境变量)
docker inspect nginx:alpine
# 删除镜像(先删引用它的容器)
docker rmi nginx:alpine
# 删除悬空镜像(构建过程中产生的 <none> 镜像)
docker image prune -f
# 一键清理所有未使用的镜像、容器、网络、构建缓存
docker system prune -a
排错:删除镜像提示 container is using it。说明还有容器在引用该镜像,先删容器再删镜像:
docker ps -a | grep nginx
docker rm -f <容器ID>
docker rmi nginx:alpine
四、容器生命周期管理实战
掌握了镜像,就可以运行容器。下面用 Nginx 举例,覆盖前台/后台运行、端口映射、命名、进入容器和日志查看。
# 前台运行并打印日志(Ctrl+C 退出)
docker run --rm -p 80:80 nginx:alpine
# 后台运行(-d)、命名(--name)、端口映射(-p 宿主机:容器)、挂载目录(-v)
docker run -d \
--name web \
-p 80:80 \
-v /var/www/html:/usr/share/nginx/html:ro \
--restart=always \
nginx:alpine
# 查看运行中的容器
docker ps
# 查看所有容器(含已退出)
docker ps -a
# 查看容器实时日志,-f 跟随输出,--tail 只看末尾 N 行
docker logs -f --tail 100 web
# 进入容器执行命令(多数精简镜像没有 bash,用 sh)
docker exec -it web sh
# 容器内查看进程
docker exec web ps aux
# 停止 / 启动 / 重启 / 删除容器
docker stop web
docker start web
docker restart web
docker rm -f web
端口映射说明:-p 80:80 表示把宿主机的 80 端口转发到容器的 80 端口。若宿主机 80 已被占用,可改用 -p 8080:80 或换端口。写多个 -p 可映射多个端口。
--restart=always 值得注意:容器异常退出或 VPS 重启后会自动拉起,生产环境务必加上,否则系统重启后服务静默下线。
排错:容器秒退(Exited)。先看日志再下结论,不要盲目重启:
docker logs --tail 50 web
docker inspect web | grep -A 5 State
常见原因:端口冲突(宿主机端口已被占用)、应用启动参数错误、数据卷权限不足。日志里通常直接给出答案。
五、Dockerfile 构建自定义镜像
现成镜像不够用时,用 Dockerfile 构建。下面构建一个基于 Alpine 的 Python 应用镜像:
# 基础镜像
FROM python:3.11-alpine
# 设置工作目录
WORKDIR /app
# 先复制依赖清单再装依赖,充分利用构建缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目代码
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["python", "app.py"]
# 构建镜像,-t 指定名称和标签,最后的 . 是构建上下文
docker build -t myapp:v1 .
# 运行构建出的镜像
docker run -d --name myapp -p 8000:8000 --restart=always myapp:v1
构建优化建议:把经常变化的文件(代码)放在最后 COPY,把不常变的(依赖清单、编译产物)放前面,能大幅复用缓存,二次构建从分钟级降到秒级。
排错:构建时网络超时。Dockerfile 内的 RUN 是在容器里执行的,容器默认走宿主 DNS,国内源装系统包时在 RUN 前加一行设置国内源,例如 Alpine 的 apk:
RUN sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories \
&& apk add --no-cache build-base
六、docker-compose 多服务编排实战
单容器用 docker run 够用,一旦涉及 Web + 应用 + 数据库多个服务,就该上 docker-compose:用一份 YAML 声明所有服务、网络和依赖关系,一条命令整体起停。下面是一个 Nginx + PHP-FPM + MySQL 的经典 LNMP 栈:
version: "3.8"
services:
nginx:
image: nginx:alpine
container_name: lnmp_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./www:/var/www/html
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- php
restart: always
networks:
- lnmp
php:
image: php:8.2-fpm-alpine
container_name: lnmp_php
volumes:
- ./www:/var/www/html
environment:
- TZ=Asia/Shanghai
restart: always
networks:
- lnmp
mysql:
image: mysql:8.0
container_name: lnmp_mysql
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: "YourStrongPass123"
MYSQL_DATABASE: "myblog"
TZ: "Asia/Shanghai"
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: always
networks:
- lnmp
volumes:
mysql_data:
networks:
lnmp:
driver: bridge
保存为 docker-compose.yml 后,在文件所在目录执行:
# 后台启动所有服务
docker compose up -d
# 查看服务状态
docker compose ps
# 查看某个服务的实时日志
docker compose logs -f php
# 在某个服务里执行命令
docker compose exec php php -v
# 重新构建并启动(改了 Dockerfile 后)
docker compose up -d --build
# 停止并删除所有容器(数据卷默认保留)
docker compose down
# 彻底清理(连同数据卷一起删,慎用!)
docker compose down -v
compose 排错一:容器间互相访问不了。同一 compose 网络内的服务可直接用服务名当主机名,例如 Nginx 配置里 fastcgi_pass php:9000;,而不是 127.0.0.1。
compose 排错二:改了 YAML 不生效。修改配置后必须 docker compose up -d 重新应用,仅 restart 不会加载新配置;改动端口或卷时,旧容器可能需 docker compose down 后重建。
compose 排错三:时区不对。容器默认 UTC 时间,与北京时间差 8 小时,导致日志和定时任务错乱。在每个服务里加 environment: - TZ=Asia/Shanghai 即可,上面的示例已示范。
七、数据持久化与备份恢复
容器本身是"一次性"的,docker rm 之后容器内写的数据全部消失。生产环境必须把数据放到数据卷(Volume)或宿主机挂载目录中,前面示例的 mysql_data 命名卷和 ./www 目录挂载就是两种方式。
数据卷备份和恢复可以借助一个临时 Alpine 容器:
# 备份 mysql_data 卷到宿主机 /backup
docker run --rm \
-v mysql_data:/data \
-v /backup:/backup \
alpine tar czf /backup/mysql_data_$(date +%F).tar.gz -C /data .
# 恢复:先把卷里的旧数据清理掉再解压
docker run --rm \
-v mysql_data:/data \
-v /backup:/backup \
alpine sh -c "rm -rf /data/* && tar xzf /backup/mysql_data_$(date +%F).tar.gz -C /data"
数据库类容器强烈建议在容器内做逻辑备份(mysqldump),再配合数据卷备份双保险:
docker exec lnmp_mysql sh -c \
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" myblog | gzip > /var/lib/mysql/backup.sql.gz'
# 配合 crontab 定时执行,实现全自动备份
crontab -e
# 每天凌晨 2 点备份
0 2 * * * docker exec lnmp_mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" myblog | gzip > /var/lib/mysql/backup_$(date +%F).sql.gz'
八、容器资源限制与安全加固
一台 VPS 上跑多个容器时,必须给每个容器限制资源,否则一个失控进程可能拖垮整台机器。
# 限制 CPU 为 0.5 核、内存最多 512M、交换 256M
docker run -d --name web \
--cpus=0.5 \
--memory=512m \
--memory-swap=768m \
-p 80:80 nginx:alpine
# 查看容器资源占用
docker stats
安全加固的几个要点:
- 不要用 root 运行应用:Dockerfile 里加
USER nobody或在 run 时加--user 1000:1000,降低容器逃逸风险。 - 最小化暴露端口:数据库等内部服务不要映射到公网,compose 里删掉
ports,仅保留在内部网络访问。 - 只读根文件系统:对无写需求的容器加
--read-only。 - 及时更新镜像:老镜像往往带着已知漏洞,定期
docker pull新版并重建。 - 不要用 docker 组做特权操作:docker 组等效 root 权限,非必要不要把用户加进 docker 组。
九、常见故障与排错手册
汇总实战中最常遇到的 6 类问题,按"先看日志、再看状态、最后查配置"的顺序排查。
1. 容器一直重启(Restarting)
docker ps -a
docker logs --tail 100 <容器名>
多半是启动命令报错或健康检查失败。注意查看 docker inspect 里 State.ExitCode 和 State.Error,退出码 137 表示被 OOM 杀掉(内存超限),需调大 --memory;退出码 1 通常是应用自身报错。
2. 端口冲突
# 查看谁占用了端口
ss -lntp | grep :80
# 确认占用后,换端口或用 -p 指定其他端口启动
常见于同时用宝塔等面板又装 Docker,面板自带 Nginx 占用了 80 端口。
3. 磁盘空间被容器占满
# 查看 docker 占用的磁盘
docker system df
# 清理停止的容器、悬空镜像、无用网络、构建缓存
docker system prune -f
# 连带清理所有未使用镜像
docker system prune -af
平时用好本文第二节的 log-opts 日志限制,是防止日志撑爆磁盘的第一道防线。
4. 容器内无法联网或 DNS 解析失败
检查宿主 DNS 和 Docker 网络配置。给容器指定 DNS:
docker run -d --dns 223.5.5.5 --dns 8.8.8.8 -p 80:80 nginx:alpine
5. 挂载目录权限导致启动失败(Permission denied)
# 宿主机目录属主改为容器内用户 UID(以 1000 为例)
chown -R 1000:1000 /var/www/html
# 或运行时指定用户
docker run -d --user 1000:1000 -v /var/www/html:/data app
SELinux 开启的系统还可能报 Permission denied,需给挂载目录加 :z 后缀(如 -v /data:/data:z)。
6. 容器时间与宿主机不一致
docker run -d -e TZ=Asia/Shanghai ...
除了设置 TZ 环境变量,也可以直接挂载宿主时区文件:
-v /etc/localtime:/etc/localtime:ro
十、总结
Docker 化的核心收益是"一次构建、处处运行":环境交付标准化、故障隔离、秒级启停、轻松多实例。把本文的流程走一遍,你已经能在 VPS 上独立完成:安装 Docker、配置国内镜像源、管理镜像与容器、编写 Dockerfile、用 docker-compose 编排 LNMP 等常见服务栈,并具备数据持久化和基本排错能力。最后留一句经验之谈:容器里跑的是"应用进程",不是"整个系统",任何需要持久化的东西都要显式落到卷里,这样无论容器怎么重建,数据都不会丢。


