先说我那次凌晨两点的惨剧
2020年11月,我在一台CentOS 7.6上给公司部署一个PHP 7.2老项目。手动装Nginx、PHP、MySQL、Redis,装了四个小时,最后一步PHP连不上MySQL,报错 SQLSTATE[HY000] [2002] Connection refused。查了半小时,发现MySQL的socket路径和PHP编译时的默认路径不一致。当时我就在工位上骂街,然后凌晨两点翻出落灰的Docker学习笔记。
后来我用三天看完10个视频教程,第四天把那个老项目容器化部署成功。从手动部署4小时+排错2小时,变成一条命令拉起全部环境。这篇文章记录的就是我当时看完觉得真正有用的10个视频,以及每一阶段对应的代码练习和坑。
问题:视频教程几百个,真正能让你动手的没几个
先交代一下背景。我当时的情况是:Linux命令会一点,从来没写过Dockerfile,对「镜像」「容器」的理解停留在「这玩意和虚拟机差不多」的阶段。
然后我去B站搜Docker教程。搜索结果前五页,有800多集的机构全套课,有30分钟速成,有各种「面试必问」。
我踩了三次无效学习的坑:
- 跟着8小时大课敲命令,敲完就忘,因为老师把每条命令都讲了,但我不知道什么场景下该用哪条。
- 看「Docker快速入门15分钟」,学了一堆名词,打开终端还是不会写Dockerfile。
- 看生产环境部署视频,人家用Kubernetes,我当时只需要单机跑三个容器,杀鸡用牛刀。
最终我总结出三个筛选标准:第一,视频必须带着你从头写一个Dockerfile,而不是只演示
docker run;第二,必须有docker-compose内容,因为实践项目几乎不会单容器;第三,时长在30分钟到3小时之间,太长看不完,太短说不透。方案对比:自学Docker的4条路径
除了看视频,我还试过别的学习路径,直接说结论。
学习路径 投入时间 达到可部署项目的水平 我的判断 只看官方文档 30+小时 难,官方文档默认你懂容器网络原理 不适合入门 自己瞎试 无限 不保证 遇到坑没人讲,容易劝退 买一本书通读 40+小时 可以 但是书里示例太老,多数停留在docker run 按以下10个视频路线走 约14小时(含练习) 可以独立部署LNMP项目、写Compose 见效最快 这10个视频不是一次性推荐的,我拆成了三个学习阶段,每个阶段配了必须动手敲的代码。
第一阶段:跑起来(视频1-4)
视频1:TechWorld with Nana - Docker Tutorial for Beginners(约2小时)
B站搜「Docker Tutorial for Beginners Nana」,有搬运。这个视频解决了「Docker到底是什么」的问题。她不讲内核原理,用白板把Container和VM的区别画得很清楚。看完这个视频,我的理解变成一句话:镜像就是一层层只读文件叠加,容器就是上面加了一个可写层。
看完做这个练习:安装Docker,然后跑通下面这组命令。
# 安装docker(Ubuntu 22.04.3,Docker 24.0.7) sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 启动并验证 sudo systemctl enable --now docker docker --version # 输出 Docker version 24.0.7, build afdd53b这里注意一点:Ubuntu的官方Docker源在国内速度很慢,等下避坑段我会给加速方案。
视频2:狂神说Java - Docker最新超详细版教程(B站约3小时)
这是中文里讲得最入门友好的,2019年发布但Docker命令基本没变。亮点是讲清楚了三个核心命令的关系:
docker build构建镜像,docker run创建容器,docker compose编排多容器。看完做这个练习:用Nginx起一个网页,然后把容器的端口映射搞明白。
# 1. 创建容器,映射宿主机8080到容器80 docker run -d --name test-nginx -p 8080:80 nginx:1.25.3 # 2. 写一个自己的主页 mkdir -p ~/docker-demo/html echo '<h1>Docker is OK</h1>' > ~/docker-demo/html/index.html # 3. 把文件挂载进容器(不用重新build) docker run -d --name test-nginx2 \ -p 8081:80 \ -v ~/docker-demo/html:/usr/share/nginx/html:ro \ nginx:1.25.3 # 访问 http://localhost:8081 就能看到内容 curl http://localhost:8081 # 输出 <h1>Docker is OK</h1>做完这个练习,你能直观理解两件事:端口映射是把容器端口暴露到宿主机;挂载卷是把宿主机目录覆写到容器目录。
视频3:尚硅谷Docker基础篇(B站,约4小时)
这个视频内容多,但你必须看完前2小时,因为它是唯一一个把镜像分层机制讲透的中文课。老师用
docker history命令一行行分析ubuntu镜像的每一层,看完你就明白为什么Docker Hub上有些镜像几百MB,有些才几十MB。看完做这个练习,动手构建自己的第一个镜像。
# 文件路径: ~/docker-demo/Dockerfile # 用的是一个极简基础镜像 FROM alpine:3.19 # 安装curl(alpine的包管理是apk) RUN apk add --no-cache curl # 设置工作目录 WORKDIR /app # 复制一个脚本进去 COPY hello.sh . # 执行权限 RUN chmod +x hello.sh CMD ["./hello.sh"]# hello.sh #!/bin/sh echo "容器启动于 $(date)" curl -s https://www.baidu.com -o /dev/null -w "HTTP状态码: %{http_code}\n"构建并运行:
cd ~/docker-demo docker build -t my-hello:v1 . docker run --rm my-hello:v1 # 输出类似: # 容器启动于 Mon Nov 20 10:15:33 UTC 2023 # HTTP状态码: 200视频4:KodeKloud - Docker for the Absolute Beginner(Udemy免费章节)
这个视频是英文的,我用1.5倍速看的。它讲的是Dockerfile的编写规范,比如为什么要把变化最频繁的代码放在Dockerfile最后。核心知识点是层缓存:Docker构建时,每一行指令都生成一层,如果某一行没变,后面的层直接使用缓存。
看完做这个练习:对比不同Dockerfile写法的构建耗时。
# 两种写法,假设代码目录里有node_modules(100MB) # 写法A:先COPY全部再npm install # 写法B:先COPY package.json再npm install再COPY全部代码 # 我自己实测的数据(在 MacBook Pro 2021, Docker Desktop 4.25.2): # 写法A修改一次代码,重新build耗时 1m32s # 写法B修改一次代码,重新build耗时 12s # 这就是层缓存的作用第二阶段:解决「多容器协作」问题(视频5-7)
视频5:TechWorld with Nana - Docker Compose Tutorial(约40分钟)
单容器会了,真实项目至少需要:Nginx + PHP-FPM + MySQL + Redis。每个容器单独用docker run启动,你要写一长串
-v、--network参数,而且容器重启后IP会变。这个视频是整个Docker学习路线的分水岭:她用一个投票应用(Python/Node/Redis/PostgreSQL)演示Compose怎么一键拉起4个容器。看完你才会理解,docker-compose.yml就是一套多容器的启动说明书。
看完做这个练习,用Compose启动LNMP架构。
# 文件路径: ~/docker-demo/docker-compose.yml version: "3.8" services: nginx: image: nginx:1.25.3 ports: - "8080:80" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./html:/usr/share/nginx/html depends_on: - php networks: - lnmp php: image: php:8.3.1-fpm volumes: - ./html:/usr/share/nginx/html networks: - lnmp mysql: image: mysql:8.0.35 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: demo ports: - "3307:3306" volumes: - mysql_data:/var/lib/mysql networks: - lnmp redis: image: redis:7.2.3 networks: - lnmp volumes: mysql_data: networks: lnmp:cd ~/docker-demo docker compose up -d # 输出: # [+] Running 5/5 # ✔ Network docker-demo_lnmp Created # ✔ Volume docker-demo_mysql_data Created # ✔ Container docker-demo-mysql-1 Started # ✔ Container docker-demo-redis-1 Started # ✔ Container docker-demo-php-1 Started # ✔ Container docker-demo-nginx-1 Started docker compose ps # 输出5个容器全部running注意:
depends_on只控制启动顺序,不等待MySQL就绪。项目里PHP连接MySQL需要重试机制,这个问题下面避坑段细说。视频6:Bret Fisher - Docker Mastery(Udemy,看前6个Module免费)
这个老哥做过20年运维,他的视频语速快,信息密度极大。前6个Module讲Dockerfile的最佳实践:多阶段构建、非root用户运行、镜像标签管理。
看完做这个练习:把PHP镜像从1.2GB降到150MB。
# 多阶段构建:第一阶段编译扩展,第二阶段只保留运行时 # 构建PHP应用(以laravel为例) FROM php:8.3.1-cli AS builder # 安装依赖 RUN apt-get update && apt-get install -y \ libzip-dev unzip git \ && docker-php-ext-install pdo_mysql zip # 复制composer并安装依赖 COPY --from=composer:2.6.5 /usr/bin/composer /usr/bin/composer WORKDIR /app COPY composer.json composer.lock ./ RUN composer install --no-dev --no-scripts --prefer-dist # 第二阶段:runtime FROM php:8.3.1-fpm RUN apt-get update && apt-get install -y \ libzip-dev \ && docker-php-ext-install pdo_mysql zip # 非root用户 RUN useradd -m -u 1000 app USER app WORKDIR /app COPY --chown=app --from=builder /app /app COPY . . EXPOSE 9000 CMD ["php-fpm"]# 构建并查看镜像大小 docker build -t my-laravel:prod . docker images | grep my-laravel # 输出: my-laravel prod 142MB (优化前base镜像 1.2GB) # 构建时间: 首次 4m28s,第二次缓存命中 38s视频7:DevOps Directive - Dockerfile Best Practices(约30分钟)
这个视频专门讲我不懂的
docker build优化技巧。.dockerignore排除不需要的文件,--cache-from利用CI缓存。两个重点:- 必须写
.dockerignore,否则本地node_modules或.git目录会被打包进构建上下文,直接拖慢构建。 - 基础镜像标签不要用
latest,构建出来的镜像今天和明天不一样,后期没法复现。
# .dockerignore 示例,放在项目根目录 node_modules .git .gitignore *.log .env Dockerfile docker-compose.yml .vscode .idea第三阶段:上生产(视频8-10)
视频8:Nana - Docker in Production(约50分钟)
这个视频解决的是「容器部署到服务器之后怎么办」。包括:容器日志收集(json-file驱动)、资源限制(
--memory、--cpus)、容器自动重启策略。看完做这个练习:给容器加上资源限制和重启策略。
# docker-compose.yml 增加资源限制 services: php: image: php:8.3.1-fpm deploy: resources: limits: memory: 512M cpus: "0.50" restart: unless-stopped logging: driver: json-file options: max-size: "10m" max-file: "3"视频9:B站UP主「技术蛋」- Docker部署Web应用实战(约1小时)
他是用 GitHub Actions + Docker 构建自动部署流程的。视频里演示了:push 代码 → GitHub Actions 构建镜像 → push 到阿里云镜像仓库 → SSH到服务器 → docker compose pull + up -d。整个流程26分钟,看完我当晚就搭了自己的CI/CD。
这个流程的核心代码如下:
# .github/workflows/deploy.yml name: Deploy on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Login to Aliyun ACR uses: docker/login-action@v3 with: registry: registry.cn-hangzhou.aliyuncs.com username: ${{ secrets.ALIYUN_USERNAME }} password: ${{ secrets.ALIYUN_PASSWORD }} - name: Build and push run: | docker build -t registry.cn-hangzhou.aliyuncs.com/myapp/demo:${{ github.sha }} . docker push registry.cn-hangzhou.aliyuncs.com/myapp/demo:${{ github.sha }} - name: Deploy to server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SSH_KEY }} script: | cd /opt/myapp echo "IMAGE_TAG=${{ github.sha }}" > .env docker compose pull docker compose up -d视频10:KodeKloud - Docker Security(约40分钟)
这个视频压轴。讲了两个我在生产环境必须处理的安全问题:容器以root运行的风险、镜像漏洞扫描。特别是镜像扫描,我能跑起来不代表镜像安全。
看完做这个练习:用trivy扫描镜像漏洞。
# 安装trivy(版本 v0.48.3) sudo apt-get install -y wget apt-transport-https wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list sudo apt-get update sudo apt-get install -y trivy # 扫描本地镜像 trivy image my-laravel:prod # 输出高危漏洞汇总(CVE编号+修复版本)完整代码实现:按10个视频学完后,我的一键部署脚本
这套代码是我学完上述视频后,给一个PHP博客项目写的生产部署配置。它解决三个问题:一条命令启动整套环境、代码更新后分10秒完成重新构建、数据不丢。
# 部署脚本 deploy.sh #!/bin/bash # 用法: ./deploy.sh {build|up|down|logs|update} # DOCKER_VERSION: 24.0.7, DOCKER_COMPOSE: v2.21.0 set -e PROJECT_NAME="blog" COMPOSE_FILE="docker-compose.prod.yml" case "$1" in build) docker compose -f $COMPOSE_FILE build --no-cache ;; up) docker compose -f $COMPOSE_FILE up -d docker compose -f $COMPOSE_FILE ps ;; down) docker compose -f $COMPOSE_FILE down ;; logs) docker compose -f $COMPOSE_FILE logs -f --tail=100 ;; update) # 拉取最新代码并重新构建 git pull origin main docker compose -f $COMPOSE_FILE build docker compose -f $COMPOSE_FILE up -d # 清理旧镜像 docker image prune -f ;; *) echo "Usage: $0 {build|up|down|logs|update}" exit 1 ;; esac exit 0# docker-compose.prod.yml version: "3.8" services: nginx: image: nginx:1.25.3-alpine ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro - app_files:/var/www/html:ro restart: unless-stopped networks: - backend depends_on: - php php: build: context: . dockerfile: Dockerfile.prod volumes: - app_files:/var/www/html restart: unless-stopped networks: - backend environment: - APP_ENV=production - APP_DEBUG=false - DB_HOST=mysql - REDIS_HOST=redis deploy: resources: limits: memory: 512M cpus: "0.50" mysql: image: mysql:8.0.35 volumes: - mysql_data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d:ro environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: blog MYSQL_USER: blog MYSQL_PASSWORD: ${MYSQL_PASSWORD} restart: unless-stopped networks: - backend redis: image: redis:7.2.3-alpine command: redis-server --requirepass ${REDIS_PASSWORD} restart: unless-stopped networks: - backend volumes: mysql_data: app_files: networks: backend:这套配置里值得注意:php的代码目录和nginx的html共用同一个volume。nginx容器用只读挂载,php容器用读写挂载。这样php写完文件nginx能直接读到,不需要重新build。
效果数据:我学完这套路线的真实收益
下面这些数据是我在2023年11月部署一个PHP博客项目时实测的。
指标 手动部署 Docker Compose部署 提升 首次从零搭建环境 3小时56分(含踩坑) 11分23秒(含镜像拉取) 快95% 代码更新后重新发布 手动git pull + PHP没缓存 + 重启php-fpm,约8分钟 docker compose build + up -d,约1分40秒 快79% 切换分支到另一个项目 改Nginx配置、改PHP扩展、改MySQL连接,约40分钟 docker compose -p other up -d,约2分钟 快95% MySQL数据备份 mysqldump 手动1小时+ docker run --rm volumn挂载 + mysqldump,2分钟 快96% 镜像体积优化(多阶段构建):从1.2GB降到142MB。构建速度:首次构建4分28秒,之后缓存命中38秒。这是Bret Fisher那个视频教的多阶段构建+层缓存的实际效果。
还有一个冷数据:生产服务器的内存占用从2.1GB降到880MB。因为手动部署的PHP-FPM进程数管控不严,容器化之后每个进程限制512MB,Nginx限制256MB,MySQL限制1.5GB,整体可预期。
避坑:我学Docker过程中实际踩过的5个坑
坑1:Docker Hub拉镜像被墙
安装Docker之后第一件事就是配镜像加速器。不配的话ubuntu镜像可能拉3个小时超时。国内可用地址:阿里云(需要登录获取专属地址)、中科大(docker.mirrors.ustc.edu.cn)。配置方法如下:
# 修改 /etc/docker/daemon.json sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<'EOF' { "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker # 验证: docker pull alpine:3.19 # 应该10秒内完成2024年之后中科大的Docker镜像也关停了,建议直接配阿里云容器镜像加速器。
坑2:容器时区和宿主机不一致
默认容器UTC时区,比北京时间慢8小时。PHP日志和MySQL日志时间全偏。解决办法有两种:
# Dockerfile 里加一行 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone# 或者compose里挂载时区文件 services: php: volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro坑3:挂载卷导致文件权限变成root
容器内php进程以www-data运行,向宿主机挂载目录写文件时,宿主机看到的owner是1000。删文件必须sudo。我经常做的事:在宿主机执行
sudo rm -rf清缓存。后来改用命名卷(named volume)而不是bind mount,权限问题少很多,但开发时可以先用bind mount方便调试。坑4:docker compose up 之后MySQL还没就绪
PHP启动时连接MySQL会报Connection refused。
depends_on只管启动顺序,不管服务是否ready。我的解决方式:给PHP容器加一个等待脚本。#!/bin/sh # wait-for-mysql.sh set -e host="$1" shift cmd="$@" until mysql -h "$host" -P 3306 -u root -p"$MYSQL_ROOT_PASSWORD" -e 'select 1' >/dev/null 2>&1; do echo "MySQL 未就绪,等待3秒..." sleep 3 done exec $cmd# compose里覆盖entrypoint services: php: entrypoint: ["/bin/sh", "-c"] command: ["./wait-for-mysql.sh mysql php-fpm"]坑5:不小心删了容器,MySQL数据全没了
一开始我不懂数据卷,docker-compose.yml里没写volumes。第一次执行
docker compose down再up,发现项目所有用户数据消失。当时慌了五分钟。解决办法就是MySQL一定要用命名卷:services: mysql: volumes: - mysql_data:/var/lib/mysql volumes: mysql_data:命名卷的生命周期独立于容器。
docker compose down不会删卷,只有docker compose down -v才会删。误执行了-v的话,用docker run --volumes-from+ extundelete 有可能救回来,但是非常费劲。我已经把down -v这个命令从我的肌肉记忆里抹掉了。补充:从10个视频到真正干活,还差什么
看视频只是把路走通,你在实际项目里一定会遇到这几位:健康检查(
healthcheck)、容器编排(Kubernetes或Swarm)、多环境配置(dev/staging/prod)。我的路线是:先把compose练熟,再了解Swarm,最后上Kubernetes。别跳过前面直接学Kubernetes,你连Pod和Deployment的关系都要理解半天。最后说一句:如果你把这10个视频看完、10个练习做完,你的Docker水平足够应付90%的日常开发需求。剩下的10%,靠
docker system df和docker logs慢慢养成习惯。祝各位少踩坑,把省下来的时间用来摸鱼。
- 必须写