“在我电脑上明明能跑”以前是个笑话。Docker 却把它变成了一种交付办法:把应用和运行环境一起打包。这一页先讲清镜像与容器,再解释其他概念,也会处理每个初学者都会撞上的三个坑:端口被占用、数据突然消失,以及不知不觉吃掉的 40GB 磁盘空间。
🎙️ 发布并录制于: · 更新于 ·
应用要正常运行,光有代码还不够。它还需要特定版本的 Python、系统库、配置和环境变量。过去部署应用,就是在另一台机器上重新搭好这一整套东西,然后祈祷别出错。容器会把应用和完整的运行环境装进一个封闭、便携的箱子,到哪里都用同一种方式运行。它和货运集装箱是一个道理:每到一个港口,都不必把你的家具拆开重装。
# the entire pitch, in one command — run redis without installing redis:
docker run redis
# no apt install, no config files, no version conflicts with
# the redis your OTHER project needs. sealed box.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?只要弄懂这一点,Docker 就不会再显得混乱。镜像是一份冻结的模板:只读、可以分享,也有版本。容器则是镜像的一个运行实例。前者像食谱,后者像照着食谱做出的蛋糕;也像类和对象。一份镜像可以启动二十个容器,每个容器都有自己的状态。删除容器,不会伤到镜像。
docker pull python:3.12 # download an image (the template)
docker images # list templates on this machine
docker run -it python:3.12 # bake a cake: image → running container
docker ps # list RUNNING containers
docker ps -a # ...including stopped ones (they linger!)
python:3.12 的格式是名称、冒号、标签,标签就是版本。省略标签,相当于使用 :latest。这个名字全靠发布者自觉:“latest”只是发布者最后推送的版本,两次拉取之间可能悄悄发生变化。正经项目一定要固定标签,用 python:3.12,不要只写 python。
docker run 的常见用法,九成都靠四个参数。-d 表示在后台运行;--name 给容器起名,后面才能方便地引用它;-p 把端口穿过容器这堵墙;-e 传入环境变量。端口映射值得单独画张图,因为几乎每个人第一次都会看反。
docker run -d --name web -p 8080:80 -e MODE=prod nginx
-p 8080:80
└──┬─┘└┬┘
YOUR machine's port : the CONTAINER's port
"requests to localhost:8080 → into the box at :80"
mnemonic: host first, box second — outside before inside
Error ... failed to bind host port 0.0.0.0:8080: address already in use (或者 port is already allocated)docker ps 找到它,再用 docker stop 停掉;也可以换一个宿主机端口,例如 -p 8081:80。容器内部的端口不会互相冲突,真正会冲突的只有宿主机这一侧。
排查问题基本只用三组命令。logs 看容器输出了什么;exec 在容器里面打开一个命令行;stop 和 rm 负责停止和删除容器。容器“跑不起来”时,百分之九十五的答案都在日志里。容器的主进程一退出,容器也会退出,所以启动时崩溃,看起来就像“它自己停了”。
docker logs web # everything it printed
docker logs -f web # follow live (like tail -f)
docker exec -it web sh # a shell INSIDE the container
# look around: ls, cat config, ps — then exit
docker stop web # polite stop
docker rm web # remove the (stopped) container
docker run,但 docker ps 里什么都没有。容器并没消失,它只是立刻崩溃或执行完了。docker ps -a 会显示它和退出码,docker logs <name> 会告诉你原因。容器的寿命和主进程完全一样长。它不是一台在后台空转的虚拟机,而是装在盒子里的一个进程。
Dockerfile 是构建镜像的配方:先选一个基础镜像,再复制文件、执行安装命令,最后说明容器启动时要运行什么。你实际会读写的 Dockerfile,大多离不开下面这五种指令。
FROM python:3.12-slim # start from this base image
WORKDIR /app # cd (and mkdir) inside the image
COPY requirements.txt . # copy deps list first — see next section!
RUN pip install -r requirements.txt
COPY . . # now the rest of the code
CMD ["python", "app.py"] # what runs when a container starts
docker build -t myapp:1.0 . # bake it (-t = name:tag, . = here)
docker run -p 8000:8000 myapp:1.0
Dockerfile 的每一行都会产生一个层,Docker 会缓存每一层。只要某一行和它之前的内容都没变,这一层就能立刻复用。由此只需要记住一条规则,也是优化 Dockerfile 最有效的一条:越不常变的内容越靠前,越常变的内容越靠后。所以要先复制依赖清单,再复制代码。
# ✗ naive order — change one line of code,
# and pip reinstalls EVERYTHING (5 min per build):
COPY . .
RUN pip install -r requirements.txt
# ✓ cache-friendly — deps layer only rebuilds when
# requirements.txt itself changes (build: ~2 sec):
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
这也解释了两个现象:使用同一个基础镜像的多个镜像会共享磁盘空间;第一次构建很慢,第二次却几乎瞬间完成。缓存并不神秘,它就是一叠冻结的差异层。从第一行发生变化的地方开始,后面的缓存会依次失效。
这个坑真的会让人丢数据:容器被删除时,它自己的文件系统也会消失。你在容器里运行数据库,积累了一个月的数据,然后执行 docker rm,数据就全没了。数据卷是一块放在容器外部的持久化存储,再挂载到容器里面。数据库一定要使用数据卷。
# named volume — Docker manages where it lives
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# bind mount — YOUR folder appears inside the box
# (this is how dev setups live-reload code)
docker run -v ./src:/app/src myapp
docker volume ls # see your volumes
docker rm,重新启动后,看看数据还在不在。务必在生产环境替你做这次测试之前,自己先测一遍。
真正的应用通常有好几个容器,比如应用、数据库和缓存。docker compose 会读取一个 YAML 配置文件,其中描述了所有容器,然后把它们当成一个整体管理。还有一个很方便的地方:同一个 Compose 文件里的容器,可以直接用服务名找到彼此。应用连接的是 db:5432,不用写 IP 地址。
# compose.yaml
services:
web:
build: .
ports: ["8000:8000"]
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
db: # ← "db" is also its hostname
image: postgres:16
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:
docker compose up -d # start everything
docker compose logs -f web # follow one service's logs
docker compose down # stop and remove (volumes survive)
Docker 从不会主动删除东西。拉取过的每个镜像、停止的每个容器、构建留下的每个孤立层,都会悄无声息地一直留在磁盘上。半年以后,你问“磁盘怎么满了”,答案往往只有一个词:Docker。下面两个命令就能管理这些空间。
docker system df # how much space is Docker eating?
docker system prune # delete stopped containers + dangling images
docker system prune -a # ...plus ALL unused images (aggressive)
# prune never touches: running containers, their images, volumes.
# volumes need explicit: docker volume prune (careful — data!)
按照你脑子里正在问的那句话来查。
# "run this thing"
docker run -d --name x -p 8080:80 image:tag
# "what's running / what just died?"
docker ps · docker ps -a
# "why is it broken?"
docker logs -f x
# "let me look inside"
docker exec -it x sh
# "port is already allocated"
docker ps → stop the old one, or -p 8081:80
# "my build is slow"
copy deps file → install → THEN copy code
# "where did my data go" (asked once, never again)
volumes for anything that must survive: -v name:/path
# "disk is full"
docker system df → docker system prune
# "start/stop the whole stack"
docker compose up -d · docker compose down
掌握这些,就足以在工作中用好 Docker。命名空间、控制组和镜像仓库还能继续深挖,但日常每周真正会用到的,就是这一页的内容。Docker 也正好能串起整套开发工具:容器本质上是 Linux 进程,所以命令行知识在容器里照样适用;而由 Git 跟踪的 Dockerfile,已经记录了完整的部署方式。