Docker 入门教程:从安装到容器化部署 Web 应用

Docker 已经成为了现代软件部署的标配技术。它将应用及其依赖打包成一个标准化的容器,确保应用在任何环境中都能一致地运行。

很多开发者都经历过这样的场景:本地跑得好好的 Node 服务,部署到服务器上突然报"缺了某个依赖";同事电脑上能正常启动的项目,到你机器上一跑就崩。这类"环境不一致"的问题,Docker 用一个只读镜像把代码、运行时和系统库打包在一起,从根源上消除了"在我机器上能跑"的争论。下面的教程从零开始,走完"安装 → 构建镜像 → 运行容器 → Compose 编排 → 部署上云"的完整链路,所有命令都可以直接复制执行。

一、Docker 核心概念

1.1 什么是容器

传统部署:
应用 → 依赖库 → 操作系统 → 服务器
(环境差异导致"在我机器上能跑"的问题)

容器部署:
Docker 容器(应用 + 依赖包)→ Docker 引擎 → 操作系统
(标准化,环境一致)

容器经常和虚拟机放在一起比较,但两者的隔离层级完全不同。虚拟机虚拟化的是整块硬件(Hypervisor + Guest OS),一个虚机通常要占几个 GB;容器只虚拟化操作系统层,多个容器共享同一个 Linux 内核,启动只需几百毫秒,单个镜像往往只有几十到几百 MB。这也是为什么一台 4 核 8G 的云服务器能轻松跑十几个容器,却跑不动几个虚拟机。

1.2 核心术语

术语 说明 类比
镜像(Image) 应用及其依赖的只读模板 安装光盘
容器(Container) 镜像的运行实例 运行中的程序
Dockerfile 构建镜像的指令文件 食谱
Docker Hub 镜像仓库 应用商店
Docker Compose 多容器管理工具 一键启动

把三者串起来理解:docker build 按 Dockerfile 的指令把项目"烘焙"成镜像,docker run 用镜像开一个容器。容器里改坏了,删掉再开一个就是,镜像本身始终干净不变。这套"不可变镜像 + 可丢弃容器"的思路,是后面所有编排和 CI/CD 操作的地基。

二、安装 Docker

# macOS
brew install docker --cask
# 或访问 https://www.docker.com/products/docker-desktop

# Ubuntu
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER  # 免 sudo 运行 docker

# 验证安装
docker --version
docker run hello-world

装完先跑一遍 docker run hello-world,能打印出欢迎信息就说明引擎工作正常。Docker Desktop 自带可视化的 Dashboard,新手可以直观看到镜像、容器和卷的状态;服务器上一般只装 Docker Engine,用命令行管理。

参考:https://docs.docker.com/get-started/(官方入门指南)· https://docs.docker.com/engine/install/(各平台安装文档)

三、第一个 Docker 应用

3.1 创建一个简单的 Web 应用

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]
// server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello Docker!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Dockerfile 里每一行都是一层"构建指令",顺序其实有讲究:先 COPY package*.jsonRUN npm install,是为了利用层缓存——只要依赖文件没变,npm install 这一层就不用重跑;如果一上来就 COPY . .,任何代码改动都会让缓存全部失效,每次构建都要重新装一遍依赖,项目大了构建时间可能从几秒拖到几分钟。

3.2 构建和运行

# 构建镜像
docker build -t my-web-app .

# 运行容器
docker run -d -p 3000:3000 --name my-app my-web-app

# 查看运行中的容器
docker ps

# 查看日志
docker logs my-app

# 停止容器
docker stop my-app

# 删除容器
docker rm my-app

-p 3000:3000 的意思是"把宿主机的 3000 端口映射到容器内的 3000 端口",冒号左边是宿主机、右边是容器,改成 -p 8080:3000 就能用 8080 访问。-d 让容器在后台运行,--name 起个名字方便后续操作。浏览器打开 http://localhost:3000,看到 "Hello Docker!" 就成功了。

四、Docker Compose:多服务管理

4.1 什么是 Docker Compose

Docker Compose 用一个 YAML 文件定义和运行多个容器,适用于 Web 应用 + 数据库 + 缓存等多服务场景。单独跑一个 web 容器,一条 docker run 就够了;可一旦加上 MySQL、Redis、队列 worker,命令会变得又长又难维护,还容易记错启动顺序。Compose 把整套应用拓扑写进 docker-compose.yml,一条 docker-compose up -d 全部拉起,这正是LAMP 环境这类经典多容器方案的标准做法。

4.2 docker-compose.yml 示例

version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_USER=app
      - DB_PASSWORD=secret
    depends_on:
      - db
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=app
      - MYSQL_USER=app
      - MYSQL_PASSWORD=secret
      - MYSQL_ROOT_PASSWORD=rootsecret
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  db_data:

注意 depends_on 只控制启动顺序,不保证数据库"就绪"——MySQL 容器起来了,不代表能立刻接受连接。稳妥的做法是在应用里加重试逻辑,或者用 Compose 的 healthcheck 等数据库健康后再启动 web。volumes 里的 .:/app 把代码目录挂进容器,改代码不用重新构建;db_data 是命名卷,数据库文件存在卷里,容器删了数据还在。

4.3 启动多服务

# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f web

# 停止所有服务
docker-compose down

down 默认停掉容器但保留数据卷;想连数据一起清掉用 docker-compose down -v——这个命令不可逆,执行前想清楚。

五、部署到服务器

5.1 使用 Docker 部署到 VPS

# 1. 服务器上安装 Docker

# 2. 传输项目文件
scp -r ./my-app user@server:/var/www/

# 3. 服务器上运行
ssh user@server
cd /var/www/my-app
docker-compose up -d --build

5.2 使用 Docker 优化部署

# 多阶段构建(减小镜像体积)
# 第一阶段:构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 第二阶段:运行(仅包含运行所需文件)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

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

多阶段构建的核心是"只把运行需要的东西带进最终镜像"。前端项目 build 出的 dist 静态文件,可以直接交给一个轻量的 Nginx 镜像去跑,没必要把整个 Node 构建环境塞进产物里。以 Next.js 这类应用为例,多阶段构建后镜像体积通常能从 1GB+ 压到 200MB 左右,推送到镜像仓库和拉取都快得多。

六、常用 Docker 命令

# 镜像管理
docker images                    # 列出本地镜像
docker pull nginx                # 拉取镜像
docker rmi <image_id>            # 删除镜像

# 容器管理
docker run -d nginx              # 后台运行
docker exec -it <id> /bin/bash   # 进入容器
docker logs -f <id>              # 查看日志
docker cp <id>:/path /local      # 复制文件

# 清理
docker system prune              # 清理未使用的容器和镜像
docker volume prune              # 清理未使用的卷

七、最佳实践

1. 镜像瘦身:
   - 使用 Alpine 基础镜像
   - 多阶段构建
   - 合并 RUN 命令减少层数

2. 安全:
   - 不使用 root 用户运行容器
   - 定期更新基础镜像
   - 使用 .dockerignore 排除敏感文件

3. 数据持久化:
   - 数据库等有状态服务使用卷(volumes)
   - 配置文件使用挂载(bind mounts)

4. 日志:
   - 容器内日志输出到 stdout/stderr
   - 使用 Docker 的日志驱动收集

.dockerignore 容易被忽略,但对构建速度和安全性都很重要:在项目根目录写上 node_modules.git*.log.env,这些文件就不会被打进镜像或传入构建上下文,既避免把密钥带进镜像,也让 COPY . . 快很多。

八、常见问题

  • 容器里改过的文件重启就没了? 容器是临时环境,删除即丢失。要持久化的数据放进卷(volumes),代码交给镜像本身。
  • 端口被占用怎么办? 换宿主机的映射端口(如 -p 8080:3000),或先 docker ps 看是哪个容器占用了端口。
  • 镜像一直拉不下来? 检查网络与镜像源,国内可配置 registry mirror;也可以在 Dockerfile 里改用其他镜像仓库的源。
  • 容器反复重启(Restarting)? docker logs 看启动日志,多数是环境变量、端口或依赖服务的问题。

九、总结

Docker 是现代化应用部署不可或缺的工具。掌握它的基本概念和用法,就能轻松实现"构建一次,到处运行"。从单容器 Web 应用开始,逐步学习 Docker Compose 多服务编排,最终搭起完整的 CI/CD 流水线,配合合适的云服务器,整套环境就交给自动化来打理。

参考:https://docs.docker.com/get-started/(官方入门指南)