软件工程界流传最广的笑话,大概是这句:"在我电脑上是好的。"本机 Node 20,同事 Node 16,服务器还是某个上古版本——直到 Docker 出现,这句话才终于有了终结的方法。

镜像与容器:一张光盘和一台装好的电脑

概念先掰扯清楚,其实就两层:

跑一个最简单的例子,一行命令起一个 nginx:

bash
# 拉取镜像(不指定 tag 默认 latest)
docker pull nginx

# 起一个容器:命名 web,把宿主机 8080 映射到容器 80 端口
docker run -d --name web -p 8080:80 nginx

# 打开 http://localhost:8080 —— 欢迎页就在那等着你

注意 -p 8080:80:左边是宿主机端口,右边是容器端口。整个过程没装 nginx、没改系统配置,删掉容器环境就干干净净。

写你的第一个 Dockerfile

镜像从哪来?官方仓库固然香,但自己的应用还得自己打包。假设有个 Node 服务,Dockerfile 长这样:

dockerfile
FROM node:20-alpine          # 基础镜像:越小越香

WORKDIR /app                  # 容器内工作目录
COPY package*.json ./         # 先只拷贝依赖清单
RUN npm ci --omit=dev         # 装依赖(可被缓存!)
COPY . .                      # 再拷贝源码

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

两个细节值得记住:先拷贝依赖文件再拷源码,是因为 Docker 的分层缓存——源码天天改,依赖很少动,这样改代码后重建镜像不会重新装依赖。另一个是选 alpine 变体,镜像体积能小一个数量级。

bash
# 构建镜像并运行
docker build -t myapp .
docker run -d -p 3000:3000 myapp

docker-compose:一键起飞

真实项目往往不止一个容器:应用 + 数据库 + 缓存……命令行一个个 run 会疯掉。把它们写进 docker-compose.yml:

yaml
services:
  web:
    build: .
    ports: ["3000:3000"]
    depends_on: [db]
    environment:
      DATABASE_URL: postgres://wang:wang@db:5432/blog

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: wang
      POSTGRES_PASSWORD: wang
      POSTGRES_DB: blog
    volumes: ["pgdata:/var/lib/postgresql/data"]

volumes:
  pgdata:

然后只需要一句咒语:

bash
docker compose up -d        # 后台启动整套服务
docker compose logs -f web  # 盯着应用日志
docker compose down         # 全部撤收(-v 连数据卷一起删)

注意容器之间直接用服务名互访(比如 db:5432),compose 会自动搞定内部 DNS 和网络。

常用命令速查

命令作用
docker ps -a查看所有容器(含已停止)
docker images列出本地镜像
docker logs -f <容器>跟踪容器日志
docker exec -it <容器> sh钻进容器内部排查
docker system prune清理闲置镜像/容器,回收磁盘

新手最常见的坑:docker run 忘了 -p 端口映射,然后纳闷"服务明明活着怎么访问不了"。先看映射,再怀疑人生。

把环境装进集装箱之后,你会发现部署、迁移、回滚都变成了复制粘贴级别的工作。下一篇折腾记,我会聊聊怎么用 Docker 把一台旧笔记本变成家庭实验室——毕竟这可是"折腾"分类的招牌节目。🐾