“在我机器上能跑”是程序员最头疼的问题之一。今天我来分享如何用Docker构建一致的开发环境,彻底解决环境差异带来的麻烦。
传统开发环境的痛点:
Docker通过容器化解决了这些问题。每个项目有自己的容器,依赖完全隔离,环境配置代码化,新成员一条命令就能启动。
假设我们有一个Express + MongoDB的项目。
myapp/
├── src/
│ ├── app.js
│ └── routes/
├── package.json
├── .env.example
├── Dockerfile
└── docker-compose.yml
# 使用官方Node镜像作为基础
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 先复制package文件,利用Docker缓存层
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制应用代码
COPY . .
# 创建非root用户运行应用(安全最佳实践)
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# 启动命令
CMD ["node", "src/app.js"]
关键点解释:
version: '3.8'
services:
app:
build: .
container_name: myapp-api
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
- MONGODB_URI=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
env_file:
- .env
volumes:
- .:/app
- /app/node_modules # 匿名卷,避免覆盖容器内的node_modules
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
networks:
- myapp-network
mongo:
image: mongo:6
container_name: myapp-mongo
restart: unless-stopped
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=secret
- MONGO_INITDB_DATABASE=myapp
volumes:
- mongo-data:/data/db
- ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
start_period: 40s
networks:
- myapp-network
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- myapp-network
# 开发工具:MongoDB管理界面
mongo-express:
image: mongo-express:latest
container_name: myapp-mongo-express
restart: unless-stopped
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_ADMINUSERNAME=admin
- ME_CONFIG_MONGODB_ADMINPASSWORD=secret
- ME_CONFIG_MONGODB_URL=mongodb://admin:secret@mongo:27017/
depends_on:
- mongo
networks:
- myapp-network
volumes:
mongo-data:
redis-data:
networks:
myapp-network:
driver: bridge
这个配置包含:
开发时我们需要热重载,生产时不需要。可以用多个docker-compose文件:
docker-compose.yml(生产配置)
services:
app:
build: .
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
docker-compose.dev.yml(开发配置覆盖)
services:
app:
build:
context: .
dockerfile: Dockerfile.dev # 使用开发专用的Dockerfile
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev # 使用nodemon热重载
Dockerfile.dev
FROM node:18-alpine
WORKDIR /app
# 安装nodemon用于热重载
RUN npm install -g nodemon
COPY package*.json ./
RUN npm install # 开发环境安装所有依赖(包括devDependencies)
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
使用方式:
# 生产环境
docker-compose up -d
# 开发环境
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
对于生产环境,可以用多阶段构建进一步减小镜像:
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 生产阶段
FROM node:18-alpine AS production
WORKDIR /app
# 只复制生产依赖
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 从构建阶段复制编译后的代码
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/app.js"]
这样最终的镜像只包含运行所需的文件,没有源代码、没有构建工具、没有devDependencies。
# 启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f app
# 进入容器执行命令
docker-compose exec app sh
# 重建镜像
docker-compose up -d --build
# 停止并删除容器
docker-compose down
# 停止并删除容器和卷(小心!数据会丢失)
docker-compose down -v
# 查看资源使用
docker stats
如果在容器内安装依赖后,宿主机无法访问:
volumes:
- .:/app
- node_modules:/app/node_modules # 使用命名卷
volumes:
node_modules:
Mac/Windows上需要启用polling:
// package.json
{
"scripts": {
"dev": "nodemon --legacy-watch src/app.js"
}
}
检查.env文件是否在.dockerignore中。如果在,需要显式挂载:
env_file:
- .env
确保服务名正确。在docker-compose中,服务名就是主机名:
// 正确
mongoose.connect('mongodb://mongo:27017/myapp')
// 错误(localhost指向容器本身,不是宿主机)
mongoose.connect('mongodb://localhost:27017/myapp')
Docker化开发环境的好处:
关键文件:
刚开始可能会觉得麻烦,但熟练之后,Docker会大幅提升开发效率。特别是团队协作时,环境问题的减少带来的收益远超学习成本。