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*.json 再 RUN 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/(官方入门指南)