跳至主要内容
返回博客
  • 工程实践
  • 工程工具

Docker 入门:从第一个容器到 Docker Compose

从实际操作入手,认识 Docker 的常用概念、命令、镜像、数据卷和 Compose。

Docker 入门:从第一个容器到 Docker Compose

在开发和部署过程中,经常会出现应用在本地环境运行正常,但在其他环境中无法启动的情况。出现这个问题,通常是因为程序依赖的系统库、运行时版本、配置或启动命令没有和代码一起交付。

Docker 可以把应用以及它需要的环境打包成镜像,再通过镜像启动相互隔离的容器。这样在不同机器上部署时,不需要重复手动配置环境。

Docker 的功能比较多,本文先介绍日常开发中最常用的部分:运行一个现成服务、管理容器和数据、构建自己的镜像,以及使用 Docker Compose 启动多个服务。

基本概念

刚开始学习 Docker 时,最容易混淆的是镜像和容器。下面先介绍几个常用概念:

  • 镜像(Image) 是只读模板,包含文件系统、依赖和默认启动命令。
  • 容器(Container) 是镜像的一次运行实例,本质上仍是宿主机上的隔离进程。
  • 仓库(Registry) 用于存取镜像,Docker Hub 是常见的公共镜像仓库。
  • Dockerfile 是一份构建镜像的配方。
  • 卷(Volume) 用于保存不应随容器删除而消失的数据。
  • 网络(Network) 让容器之间能够按名称通信。
  • Docker Compose 用一个 YAML 文件描述并管理一组相关容器。

一个镜像可以启动多个容器,删除容器也不会删除镜像。需要长期保存的数据不能只放在容器中,而应该使用卷进行保存。

终端里的 docker 命令只是发号施令,真正干活的是 Docker daemon:它负责构建镜像、创建容器、管理网络和存储。macOS、Windows 上安装 Docker Desktop 后,这些都会准备好。

安装并确认 Docker 可用

macOS 和 Windows 初学者可以安装 Docker Desktop。Linux 可以安装 Docker Engine。安装完成后,先运行:

docker version
docker info
docker run --rm hello-world

docker version 会显示客户端和服务端的版本,docker info 会显示当前环境。最后一条会下载 hello-world 镜像、启动容器并打印欢迎信息。--rm 的意思是:命令跑完后,顺手把这个容器删掉。

如果只能看到 Client 信息,或者报 Cannot connect to the Docker daemon,一般不是命令写错了。多半是 Docker Desktop 还没启动,或者 Linux 当前用户没有访问 Docker daemon 的权限。

运行第一个真正的服务

用 Nginx 启动一个后台 Web 服务:

docker run --name web-demo -d -p 127.0.0.1:8080:80 nginx:alpine

浏览器打开 http://localhost:8080,应该能看到 Nginx 欢迎页。第一次运行时,本地还没有 nginx:alpine,Docker 会先下载镜像,再启动容器。

把这条命令拆开看:

  • docker run:从镜像创建并启动一个新容器
  • --name web-demo:给容器一个容易记住的名字。
  • -d:在后台运行。
  • -p 127.0.0.1:8080:80:把宿主机的 8080 端口映射到容器的 80 端口,并只绑定本机地址。
  • nginx:alpine:镜像名是 nginx,标签是 alpine

端口的顺序永远是 宿主机端口:容器端口127.0.0.1 表示只让本机访问;如果直接写 -p 8080:80,通常会监听机器的所有地址。开发时别一不小心把数据库或管理端口暴露出去。

查看、调试和管理容器

容器跑起来后,最常用的是下面几条命令:

# 只看正在运行的容器
docker ps

# 查看所有容器,包括已经停止的
docker ps -a

# 持续查看日志,Ctrl+C 只会退出日志跟踪
docker logs -f web-demo

# 在运行中的容器内启动一个 shell
docker exec -it web-demo sh

# 查看容器的完整配置与状态
docker inspect web-demo

docker exec 不会新建容器,它只是在已经运行的容器里执行一条命令。很多精简镜像只有 sh,没有 bash

容器生命周期可以用下面几条命令控制:

docker stop web-demo
docker start web-demo
docker restart web-demo
docker rm web-demo

docker stop 只是停掉容器,容器本身还在,所以能用 docker start 再次启动。docker rm 用来删除已经停止的容器。运行中的容器也能用 docker rm -f web-demo 强制删除,但平时最好先正常停止,给应用一点收尾时间。

这里有一个非常关键的区别:

  • docker run nginx:alpine 会创建一个新的容器。
  • docker start web-demo 会重新启动一个已有的容器。

反复执行 docker run 后看到一堆停止的容器,并不是 Docker 出错了,而是你每次都新建了一个实例。

管理镜像

镜像和容器要分开管:

# 下载镜像,但不创建容器
docker pull nginx:alpine

# 查看本地镜像
docker image ls

# 查看镜像详情
docker image inspect nginx:alpine

# 删除不再使用的镜像
docker image rm nginx:alpine

完整的镜像名可以写成 仓库地址/命名空间/名称:标签。不写仓库地址时,默认从 Docker Hub 下载;不写标签时,通常会用 latest。不过 latest 只是个普通标签,不代表会自动更新,也不保证每次都是同一个版本。练习时可以用,线上环境最好写明版本;如果需要完全可复现,还可以固定 digest。

容器会消失,重要数据不能跟着消失

容器里虽然能写文件,但那块空间更适合放临时数据。数据库、上传文件这类重要内容别放那里:容器一删,里面的可写数据也会跟着没掉。

Docker 里常见的挂载有两种:

  • 命名卷由 Docker 管理位置,适合数据库和需要长期保存的数据。
  • 绑定挂载把宿主机的某个路径映射进容器,适合开发时同步源码或配置文件。

先用命名卷验证数据持久化:

docker volume create demo-data

docker run --rm \
  --mount type=volume,source=demo-data,target=/data \
  alpine sh -c 'echo "hello, volume" > /data/message.txt'

docker run --rm \
  --mount type=volume,source=demo-data,target=/data \
  alpine cat /data/message.txt

第一个容器退出后已经被 --rm 删掉了,第二个容器却还能读到文件,说明数据不在容器里。查看和删除卷可以用:

docker volume ls
docker volume inspect demo-data
docker volume rm demo-data

绑定挂载适合把当前目录作为静态网站交给 Nginx:

docker run --rm -d --name local-site \
  -p 127.0.0.1:8080:80 \
  --mount type=bind,source="$PWD",target=/usr/share/nginx/html,readonly \
  nginx:alpine

改完当前目录的文件,容器马上就能看到变化。readonly 能防止容器反过来改宿主机文件。绑定挂载依赖本机路径,命名卷则更容易在不同环境里复用。

用 Dockerfile 构建自己的镜像

假设你有一个 Node.js 项目,依赖写在 package.json 和 lockfile 里,入口文件是 server.js。在项目根目录新建这个 Dockerfile

Dockerfile
FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node

CMD ["node", "server.js"]

这些指令分别做什么:

  • FROM 选择基础镜像。
  • WORKDIR 设置后续命令和容器启动后的工作目录。
  • COPY 把构建上下文中的文件复制进镜像。
  • RUN 在构建时执行命令,并生成新的镜像层。
  • ENV 设置默认环境变量。
  • EXPOSE 说明应用监听哪个容器端口,但不会自动把端口开放出去
  • USER 避免应用进程以 root 身份运行。
  • CMD 定义容器默认怎么启动;JSON 数组写法能更好地接收停止信号。

先复制依赖清单、安装依赖,最后再复制源码,是为了利用构建缓存。以后只改业务代码时,安装依赖那一步通常就不用重跑。还要加一个 .dockerignore,别把无关文件或敏感文件一起交给 Docker:

.dockerignore
node_modules
.git
.env*
npm-debug.log*

然后构建并运行镜像:

docker build -t hello-docker:1.0 .
docker run --rm -d --name hello-app -p 127.0.0.1:3000:3000 hello-docker:1.0
docker logs -f hello-app

-t 是给镜像起名字、打标签;最后的 . 是构建上下文。Docker 只能从这里读取 COPY 要用的文件,所以别随手把文件特别多的上级目录当作上下文。

环境变量可以在运行时传,例如 docker run -e PORT=3000 ...。密码、令牌和私钥别写进 Dockerfile,也别复制进镜像。镜像层会被缓存、分发,后来把文件删掉也不等于它从历史层里消失了。

用 Docker Compose 管理多个服务

应用一旦还要数据库、缓存或消息队列,多条 docker run 很快就看不明白了。Compose 可以把服务、网络和卷都写进一个 compose.yaml

下面这个例子会在本机启动 WordPress 和 MySQL,正好可以看看多个容器怎么配合:

compose.yaml
services:
  wordpress:
    image: wordpress:latest
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: app
      WORDPRESS_DB_PASSWORD: dev-only-password
      WORDPRESS_DB_NAME: app
    depends_on:
      - db

  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: app
      MYSQL_USER: app
      MYSQL_PASSWORD: dev-only-password
      MYSQL_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - db-data:/var/lib/mysql

volumes:
  db-data:

compose.yaml 所在目录运行:

# 创建并在后台启动全部服务
docker compose up -d

# 查看服务状态和日志
docker compose ps
docker compose logs -f

# 在指定服务的容器中执行命令
docker compose exec wordpress php --version

# 停止并删除容器和默认网络,保留命名卷
docker compose down

Compose 默认会给项目建一个网络。wordpress 容器通过服务名 db 访问数据库,所以这里写 db:3306,不是 localhost:3306。容器里的 localhost 指的是它自己,这个坑刚开始很常见。

depends_on 只保证启动顺序,不保证数据库已经初始化完。真实项目里要给依赖服务加 healthcheck,应用本身也要能重试连接。

执行 docker compose down -v 会连声明的命名卷一起删除,示例中的数据库数据也会消失。先确认数据不再需要,再加 -v

例子里把密码直接写出来,只是为了方便本地练习。实际项目请用不提交到仓库的环境文件、Compose secrets 或部署平台的密钥管理功能。

一张日常命令速查表

目的命令
创建并启动容器docker run [选项] 镜像
查看运行中的容器docker ps
查看所有容器docker ps -a
查看日志docker logs -f 容器
在容器中执行命令docker exec -it 容器 sh
停止 / 启动容器docker stop 容器 / docker start 容器
删除容器docker rm 容器
查看镜像docker image ls
构建镜像docker build -t 名称:标签 .
查看卷docker volume ls
启动 Compose 项目docker compose up -d
查看 Compose 日志docker compose logs -f
停止 Compose 项目docker compose down
查看磁盘占用docker system df

磁盘快满时,先用 docker system df 看看是谁占了空间,再按需要删容器、镜像或卷。docker system prune 会一口气清理没在用的资源,运行前要确认;尤其别随便加 --volumes

刚开始记住这五件事就行

  1. 镜像是模板,容器是运行实例;run 创建新实例,start 启动旧实例。
  2. -p 的顺序是宿主机端口在前、容器端口在后;EXPOSE 本身不发布端口。
  3. 容器应该可以随时重建,重要数据交给卷或外部存储。
  4. 同一 Compose 网络中的服务用服务名通信;容器里的 localhost 只代表它自己。
  5. Dockerfile、Compose 文件和明确的镜像版本应该进入版本控制,密钥不应该进入镜像或仓库。

这些先用熟,不用急着背更多命令。出问题时可以按这个顺序查:容器有没有在跑 → 日志怎么说 → 端口有没有映射对 → 挂载对不对 → 服务名和网络对不对。大部分入门问题都能这样查出来。

继续学习

上面都是 Docker 官方文档。命令参数和镜像版本会变,准备上生产前,记得再核对对应版本的说明。