常用的 Docker 命令
日常那几十条按「要办的事」分组:先起容器、再看它活着没有、进去敲两下、管镜像、护住数据、Compose、清垃圾,一条命令一行作用,外加几组真的会敲出来的组合。
Docker 的命令不难,难的是同一个动词在镜像、容器、卷、网络四个对象上各有一套,背下来也会串。所以这份清单不按字母排,按"我现在要办什么事"排:先起容器,再看它活着没有,看不对劲就进去敲两下,最后管镜像和数据。参数记不住就敲 <命令> --help,那一屏就是全集,比翻笔记快。
其中有一组是「几组我会敲出来的形状」——单条命令记不住没关系,那几段组合记住了,单条就会顺出来。
先分清四个对象
- 镜像 image —— 只读的模板,一层一层叠出来的;它有版本号(tag),没有状态
- 容器 container —— 镜像跑起来的那个实例,有自己的 ID、状态、文件系统上层,删掉就没了
- 卷 volume —— 专门放数据的、不由容器生命周期管的地方;数据库的落盘应该在这儿
- 网络 network —— 容器之间怎么互相找到;同一张网络里可以用服务名当域名
命令的写法是 docker <对象> <动作>,比如 docker image ls、docker container rm。早先的简写(docker ps、docker images)都还能用,下面的清单里两种都按最常见那种写。
装完先跑这几条
docker version—— 客户端和守护进程的版本;两边差太远会有些命令报看不懂docker info—— 一整屏运行环境:存储驱动、cgroup 版本、代理、日志驱动、跑着几个容器docker info --format '{{.ServerVersion}}'—— 只取一个字段,-f吃的是 Go 模板docker compose version—— Compose 是插件,不在 daemon 里,单独确认它在docker context ls—— 当前-h会连哪台机器;本地和远端切换就靠这个docker events—— 实时打 daemon 上发生的事,查"谁在起容器"时有用docker --help/docker ps --help—— 参数全集都在这一屏里,别背
起一个容器:run 的那几个必带参数
docker run = 拉镜像(没有就拉)+ 建容器 + 启动,三段合一,所以参数最多。
-d—— 后台跑,只回一个容器 ID;不加就占住终端,Ctrl-C直接把容器停掉--name blog—— 给它一个你能记住的名字,后面所有命令都用它而不是那串哈希-p 8080:80—— 把宿主端口映射到容器端口,冒号左边是外面访问的、右边是容器里监听的-p 127.0.0.1:8080:80—— 只绑本机回环,不让它露在局域网口上;反代在前面时该这么写-p 80—— 只写一边,随机给一个宿主端口,用docker port 容器查分到哪个-v /data:/var/www:ro—— 挂宿主目录进去,ro是只读-v mydata:/var/lib/postgresql/data—— 左边不是路径就是一个卷,数据跟着卷活-e TZ=Asia/Shanghai—— 传环境变量;密钥别写在这里,会留在docker inspect里--env-file .env—— 一整个文件灌进去,参数少一半--restart unless-stopped—— 开机自启、崩了自动拉起;手动stop的那次不复活--restart always—— 同上,但手动stop也会在 daemon 重启时把它拉起来--rm—— 退出就删容器,跑一次性任务时用,省得堆一地尸壳-it—— 分配终端并进入交互,跑 shell 必备;-d和-it不一起用--memory 512m/--cpus 1—— 限额;没有这行,一个失控的进程能把整台机器拖走--log-opt max-size=10m --log-opt max-file=3—— 日志封顶,默认是无限增长的--add-host 名字:IP—— 往容器的/etc/hosts里塞一行--cap-drop ALL --cap-add NET_ADMIN—— 先把能力全去掉,再按这条服务真正需要的一项项给回来--privileged—— 别顺手加。它把宿主设备、内核模块、全部能力都给了容器--device /dev/ttyUSB0—— 直通设备;--gpus all要另装 NVIDIA Container Toolkit-w /app 镜像 命令—— 指定工作目录、覆盖启动命令--entrypoint sh 镜像 -c '命令'—— 换掉入口点,用来在镜像里跑一条不相干的检查
一条常见的形状:
docker run -d --name blog \ -p 127.0.0.1:8080:80 \ -v myblog:/app/data \ -e TZ=Asia/Shanghai \ --restart unless-stopped \ --log-opt max-size=10m --log-opt max-file=3 \ nginx:1.27看它在不在、在干什么
docker ps—— 正在跑的docker ps -a—— 含已退出的;"我明明起了怎么没起来"先看这一条docker ps -a --filter status=exited—— 只看退出的docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'—— 想要什么样的表头自己拼docker ps -q—— 只出 ID,配合xargs批量操作docker port blog—— 这个容器把哪些端口映射到了宿主docker logs blog—— 打日志;容器停了也能看docker logs -f --tail 200 blog—— 只从最近两百行开始追,不要一上来就从头灌docker logs --since 30m blog—— 最近三十分钟;--until同样能配docker inspect blog—— 一大坨 JSON,什么都在里面:IP、挂载、退出码、启动参数docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' blog—— 崩溃排查先问这两个docker stats --no-stream—— 每个容器现在的 CPU / 内存 / 网络 / 块设备docker top blog—— 容器里跑了哪些进程(宿主上看的,不需要进去)docker diff blog—— 这个容器在镜像之上改了哪些文件;找"它到底往哪写"最快
进容器里敲命令
docker exec -it blog sh—— 进去。先在镜像里试sh再试bash:alpine 系没有 bashdocker exec -u root blog sh—— 换个用户进去,很多镜像默认不是 rootdocker exec blog 命令—— 不交互,只拿输出,脚本里用这一条docker exec -w /app blog ls—— 指定进去之后站在哪个目录docker attach blog—— 接回容器的标准输入输出;Ctrl-P Ctrl-Q是脱离,Ctrl-C会真的把进程打断,别在跑前台任务时用docker cp blog:/etc/nginx/nginx.conf ./—— 从容器拷出来,反向也一样;容器停了也能拷docker commit blog blog:debug—— 把当前容器封成一个新镜像。排查时临时用, 别拿它当部署手段——那样 Dockerfile 和镜像就分家了
停、启、删
docker stop blog—— 发SIGTERM,等十秒还没退就补SIGKILLdocker stop -t 30 blog—— 给它三十秒优雅退出(跑数据库的该加大这个数)docker kill blog—— 直接SIGKILL,没有优雅可言docker kill --signal=HUP blog—— 只发一个信号不停容器,比如让 nginx 重载配置docker start blog—— 起已经建好的容器,参数不再变;-i是把标准输入一直接上docker restart blog—— 停再起;改环境变量没用,那些在创建时就定了docker pause/unpause—— 用 cgroup freezer 冻住 / 解冻,进程还活着但不跑docker rename blog blog-old—— 改名,不影响它的 IDdocker wait blog—— 阻塞到它退出,打印退出码,脚本里等一个任务时用docker rm blog—— 删容器;正在跑得先stop,或者docker rm -f blogdocker container prune—— 把所有已退出的容器一次清掉,会先问你一句
删容器不会删卷,也不会删镜像。数据在不在,看下一组。
镜像:拉、看、造、推
docker pull nginx:1.27—— 拉这个 tag;不写 tag 就是latest,而它只是一个默认标签名, 不等于"最新一版"——要版本就写版本docker images—— 本地有哪些镜像,占多少空间docker images -f dangling=true—— 没有 tag 的孤儿层,构建换了几轮之后一堆这种docker image inspect nginx:1.27—— 看它的层、入口点、暴露端口、环境变量docker history nginx:1.27 --no-trunc—— 每一层是哪条指令来的、各占多大; 想知道镜像为什么这么肥就查这个docker rmi nginx:1.27—— 删镜像;有容器在用会删不掉,-f是拆标签不是真删docker build -t blog:1.0 .—— 用当前目录的 Dockerfile 构建,最后一个点是构建上下文docker build --no-cache -t blog:1.0 .—— 跳过缓存重来,改了COPY却没生效时用docker build -f Dockerfile.dev -t blog:dev .—— 换一份 Dockerfiledocker build --build-arg VERSION=1.2.3 -t blog:1.2.3 .—— 传构建参数docker tag 旧名 注册表/名:tag—— 打上要推出去的那个名字docker login—— 推之前先登录。匿名拉取会被限流,症状是429 Too Many Requests, 登录后配额宽得多docker push 注册表/名:tag—— 推上去;推一个别人已经推过的官方镜像会直接拒docker save -o nginx.tar nginx:1.27—— 把镜像导出成 tar(带层、带元数据)docker load -i nginx.tar—— 从 tar 装回镜像;内网机器之间搬镜像走这一对docker export 容器 > fs.tar—— 导出的是容器文件系统,没有层也没有启动命令, 和save不是一回事,别混用docker image prune -a—— 删掉没有任何容器在用的镜像,会先列一遍再问
跨架构那一条得单独记:
docker buildx build --platform linux/amd64,linux/arm64 \ -t 注册表/blog:1.0 --push .--push(或者 --output type=registry)是必需的——多平台镜像没法落到本地那一份 docker images 里。
数据与卷:容器删了东西还在
docker volume ls—— 有哪些卷docker volume inspect mydata—— 它真实落在宿主哪个目录(Mountpoints那一行)docker volume create mydata—— 先建再挂,参数能写在创建时docker volume rm mydata—— 删卷。这一下才是真的把数据删了,删容器不会docker volume prune—— 清没人挂的卷。新版本默认只动匿名卷,具名那些要加--all才被扫进去——反过来说,忘了名字的卷就是被这一条带走的--mount type=volume,source=mydata,target=/data—— 和-v同一件事,写法啰嗦但参数不会串--mount type=tmpfs,destination=/app/tmp—— 只在内存里,重启就没,适合临时缓存
备份和恢复对称着记最不容易错:
# 备份:卷挂到 /from,当前目录挂到 /to,写一个带日期的包docker run --rm -v mydata:/from -v "$PWD":/to \ alpine tar czf /to/mydata-$(date +%F).tgz -C /from .
# 恢复:方向反过来,解到那个卷里docker run --rm -v mydata:/to -v "$PWD":/from \ alpine sh -c 'tar xzf /from/mydata-2026-10-26.tgz -C /to'网络:谁找得到谁
docker network ls—— 有哪些网络;bridge是默认那张docker network inspect bridge—— 谁在这张网上、各自的 IPdocker network create blog-net—— 建一张自己的,容器之间就能用名字互访docker network connect blog-net blog—— 把一个已经跑着的容器加进去,不用重建docker network disconnect blog-net blog—— 反过来docker network rm blog-net—— 还有容器挂着就删不掉docker run --network blog-net ...—— 起的时候指定docker run --network host ...—— 直接用宿主网络,没有端口映射这回事,也不隔离docker run --network container:nginx ...—— 和某个容器共用网络栈,两个进程共享一个 IP, 边车(sidecar)模式就是这个docker run --dns 1.1.1.1 ...—— 容器里的解析不对时先试这个- Compose 里同一个
services下的服务默认就在同一张网上,服务名就是域名
Compose:一个文件管一组
docker compose(中间一个空格)是现在的写法;带横杠的 docker-compose 是退役的 v1,
新项目里别再敲它。文件名 compose.yml / compose.yaml / docker-compose.yml 都认,
新项目就写 compose.yml。
最小的形状就这么点东西,一个服务一段:
services: web: image: nginx:1.27 ports: - '127.0.0.1:8080:80' volumes: - ./dist:/usr/share/nginx/html:ro restart: unless-stopped下面这些命令都是围着这一份文件转的。
docker compose up -d—— 按文件建好并后台起;改了配置也常敲这条docker compose up -d --build—— 起之前重新构建本地那个服务docker compose up -d --force-recreate—— 参数没变但想彻底换个新容器docker compose down—— 停并删掉这一组容器和网络,卷留着docker compose down -v—— 连卷一起删,数据就没了;这行不该出现在生产机上docker compose down --remove-orphans—— 把文件里已经不再列出、但还活着的容器收掉docker compose ps—— 这一组现在的状态docker compose logs -f web—— 追某一个服务的日志;不写服务名就是全部docker compose logs --since 10m --tail 100—— 别每次都从头灌docker compose restart web—— 只重起那一个docker compose pull && docker compose up -d—— 升级的标准两步docker compose build --no-cache web—— 重新构建,不吃缓存docker compose config—— 把最终合成的配置打出来(变量已代入、默认值已补齐); 怀疑.env没生效就先看这个docker compose config --quiet—— 只校验语法,不合法就非零退出,适合放进脚本docker compose exec web sh—— 进某个服务的容器docker compose run --rm web 命令—— 用同一套配置临时跑一个一次性任务(迁移、初始化)docker compose top—— 这一组里所有进程docker compose stop/start/kill—— 三种力度:优雅停、起回来、直接杀docker compose rm -sf—— 删掉这一组里已经停的容器docker compose scale web=3—— 副本数;端口有写死的宿主端口时会冲突,得改成范围写法
我实际会写的那一份,比最小形状多了这几样:具名卷、健康检查、显式网络、日志封顶、
启动顺序。密钥走 env_file,不写进这一份文件里(environment 的值会留在 docker inspect)。
services: api: build: ./api init: true env_file: .env environment: TZ: Asia/Shanghai volumes: - apidata:/data stop_grace_period: 30s healthcheck: test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:8080/healthz || exit 1'] interval: 30s timeout: 3s retries: 3 start_period: 10s logging: driver: json-file options: max-size: '10m' max-file: '3' networks: [backend]
web: image: nginx:1.27 ports: - '127.0.0.1:8080:80' volumes: - ./dist:/usr/share/nginx/html:ro depends_on: api: condition: service_healthy restart: unless-stopped networks: [frontend, backend]
networks: frontend: backend: internal: true
volumes: apidata:两处要盯的:
internal: true那张网出不去外网——挂在上面的服务互相能通、但访问不了外面,也出不去。 数据库这类"只被本组里的人连"的放这儿正合适,别顺手把自己要拉包的服务也挂上去。healthcheck那条命令必须在那个镜像里真跑得通。上面写的是带 busybox(有wget)的 自己 build 出来的服务;换个基础镜像之前先量一下里面有什么:docker exec -it 容器 sh -c 'command -v wget curl'。两样都没有时,宁可写disable: true把健康检查关掉,也别留一条永远失败的检查——那会把depends_on的condition: service_healthy卡住,整组起不来。
清垃圾:先看用量,再动手
容器越跑越多、盘越用越满,八成是这三样堆出来的:退出的容器、没标签的镜像、构建缓存。
docker system df—— 分类统计镜像 / 容器 / 卷 / 构建缓存各占多少docker system df -v—— 展开到每一条,找出"那个 3G 的镜像是谁"docker container prune—— 清已退出的容器docker image prune—— 只清孤儿层(没标签的);加-a就清所有没被容器用着的镜像docker builder prune—— 清构建缓存,构建盘满通常是它docker volume prune—— 清没人挂的卷,这一下就包括那些你忘了名字的数据卷docker system prune—— 一次清掉停用的容器 + 悬空镜像 + 构建缓存 + 没人挂的网络,不含卷docker system prune -a --volumes—— 全清,包括卷。除非你确定这台机器上没有需要留着的数据, 别敲这条;它不会提醒第二遍
安全的顺序是:先 df,再单独 container prune,看还剩多少,最后才决定要不要动 image prune -a。
docker system dfdocker container prunedocker image prunedocker builder prune几组我会敲出来的形状
一、拿一个临时容器试件事,试完不留痕迹:
docker run --rm -it alpine:3.20 sh二、只看一眼某个镜像的默认启动命令和环境变量,不用真的起:
docker inspect --format '入口点 {{.Config.Entrypoint}} 命令 {{.Config.Cmd}}' nginx:1.27docker inspect --format '{{json .Config.ExposedPorts}}' nginx:1.27三、容器起不来,三步定位:
docker ps -a --filter name=blog --format '{{.Status}}'docker logs --tail 100 blogdocker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}' blog四、把跑着的容器和宿主端口对上号,查"这个端口被谁占了":
docker ps --format '{{.Names}}: {{.Ports}}'ss -lntp | grep 8080五、升级一个 Compose 服务,出问题能立刻退回:
docker compose logs --tail 50 web > /tmp/web-before.logdocker compose pull webdocker compose up -d webdocker compose ps六、改完 Dockerfile 确认缓存真的没骗你:
docker build --no-cache -t blog:test .docker history blog:test --no-trunc | head权限那件事
docker 这个命令连的是本地的守护进程 socket,而 socket 默认只有 root 可读。把自己加进 docker 组等于给了这台机器的 root 权限——组里每一个人都能挂宿主根目录进容器,然后随意读写。共用机器时这一点要想清楚,宁可留着 sudo。
sudo usermod -aG docker $USER—— 加组,然后重新登录才生效(或者newgrp docker)getent group docker—— 看组里现在都有谁docker info --format '{{.SecurityOptions}}'—— 守护进程开了哪些安全选项
忘了参数的时候
docker ps --help—— 每一层命令都带一份完整参数说明,比搜得快docker compose config—— 想知道 compose 文件最终变成了什么docker inspect—— 想知道某个对象现在是什么状态docker events—— 想知道刚才那一下到底发生了什么journalctl -u docker --since "10 min ago"—— 守护进程自己的日志;容器起不来的原因有时在这儿而不在docker logs里
命令串得再熟,也只是把"该问 daemon 的事"问出来。真正少踩坑靠的是前面那几条习惯:数据放卷里、端口只绑回环、日志设上限、清理之前先看一眼 docker system df。