使用 Docker 部署网站的完整指南:从开发到生产

"代码在我电脑上跑得好好的,怎么一部署就出问题?"几乎每个亲手部署过网站的人都听过这句话。问题往往不在代码本身,而在于环境:本地的 Node 版本、系统依赖、数据库配置和服务器上的完全不同。Docker 的解决思路,是把「运行环境」本身也变成代码——提交到仓库、随处复现、随时回滚。Docker 容器化让网站部署从「手工配置」转变为「代码化管理」。这篇指南不讨论抽象概念,直接用 前端 + 后端 + 数据库 的完整项目,带你从 Dockerfile 一路走到生产部署。

项目结构

一个典型的容器化网站项目长这样:

project/
├── frontend/       # 前端应用(Nginx + 静态文件)
├── backend/        # 后端 API(Node.js/Python/Go)
├── docker-compose.yml
└── .env

代码按职责拆成 frontend 和 backend 两个目录,各自维护一份 Dockerfile,前端构建、后端服务、数据库就能独立扩容、独立更新。.env 集中存放数据库密码、密钥等敏感信息,并被 .gitignore 排除,避免机密进入版本库。

前端 Dockerfile:多阶段构建

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

多阶段构建是前端镜像缩身的关键。第一阶段 buildernode:20-alpine 安装依赖并执行 npm run build;第二阶段 nginx:alpine 只从构建阶段拷贝产物。最终镜像里既没有源码也没有 node_modules,体积能从 1GB+ 压缩到几十 MB,依赖少了,安全漏洞面也随之减小。npm cinpm install 更适合 CI:它严格按照 package-lock.json 安装,保证本地与线上依赖完全一致。这份镜像还能直接用于本地联调,省去反复安装环境的时间。

后端 Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

后端生产镜像不需要 devDependencies,npm ci --production 只装运行时依赖。如果你用的是 Python,把 RUN npm ci --production 换成 RUN pip install --no-cache-dir -r requirements.txt 即可,思路完全一样:锁版本、只装生产依赖、用非 root 用户运行进程(Dockerfile 里加一行 USER node)。

另外别忘了准备一份 .dockerignore,把 node_modulesdist.git*.log 排除在构建上下文之外。它有两个作用:一是避免本地依赖被错误地带进镜像,二是大幅减少发送给 Docker 守护进程的文件量,让构建更快。

Docker Compose 编排

version: "3.8"
services:
  nginx:
    build: ./frontend
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - backend

  backend:
    build: ./backend
    env_file: .env
    environment:
      - NODE_ENV=production
    restart: always

  postgres:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    env_file: .env

volumes:
  pgdata:

Compose 把三个服务串成一台"虚拟服务器"。几个细节值得注意:volumes: pgdata: 用命名卷持久化数据库,容器删了重建数据还在;env_file: .env 把环境变量统一注入;restart: always 让进程崩溃或服务器重启后自动拉起。前后端绑定到同一网络后,Nginx 里直接用服务名 backend:3000 访问即可,不用关心容器的 IP 地址。

配置反向代理

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    location / {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /static/ {
        alias /usr/share/nginx/html/static/;
        expires 365d;
    }
}

80 端口一律 301 跳转到 HTTPS,静态资源加 expires 365d 走浏览器缓存,动态请求反代到后端。SSL 证书可以用 certbot 自动续期:certbot certonly --webroot -w /usr/share/nginx/html -d example.com,再把证书目录挂载进容器。

接入 CI/CD

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push Docker image
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker push myapp:${{ github.sha }}
      - name: Deploy to server
        run: |
          ssh user@server "cd /app && docker-compose pull && docker-compose up -d"

CI/CD 的价值在于把"构建—推送—部署"变成一次 git push 自动完成的流水线。镜像用 commit SHA 打标签,回滚时只需 docker-compose up -d 指回上一个标签,几秒钟就能完成。

健康检查与优雅退出

生产环境不能只看"容器起来了",还要确认服务真的可用。给后端加一个健康检查接口,并在 Compose 里声明:

backend:
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
    interval: 30s
    timeout: 5s
    retries: 3

Nginx 的 depends_on 默认只等容器启动,配合 healthcheck 才能等"服务就绪"。restart: always 配合健康检查,可以让服务挂了之后自动拉起,而不是把请求继续转发给一个半死不活的后端。

环境变量与密钥管理

env_file: .env 解决的是"配置不进镜像"的问题:镜像里不写死数据库密码,运行时从环境变量注入。实践建议:.env 永远不进 Git;dev/staging/prod 各维护一份 .env;敏感值不要在 docker-compose.yml 里明文写。需要更严格的安全要求时,可以引入 Docker secrets 或云厂商的密钥管理服务,把密钥生命周期管起来。这些做法合起来,就是让镜像可移植、配置可替换、密钥可审计。

常见问题排查

  • 容器启动即退出:先看日志 docker logs <container>,再确认 CMD 是否前台运行。Nginx 需要 daemon off;,否则容器会立刻退出。
  • 端口冲突docker ps 查看已占用端口,或改 Compose 里 ports 左侧的宿主机端口。
  • 数据丢失:确认数据库用了命名卷,不要在容器内写重要数据。
  • 构建缓慢:把 package*.json 单独 COPY 并先 RUN npm ci,利用 Docker 层缓存,源码改动不会触发依赖重装。

16IDC 观察

Docker 化部署极大减少了环境不一致导致的问题。建议新项目从一开始就使用 Docker 和 Docker Compose 管理部署流程,即使是一个简单的静态网站,Docker 也能提供更清晰的发布和回滚流程。容器时代的部署,拼的不是单点脚本,而是这套可重复的"构建—编排—监控"链路。镜像构建好之后,还需要一台合适的服务器来承载,可以参考我们的 服务器推荐

参考:Docker 官方文档 https://docs.docker.com/build/building/multi-stage/;Docker Compose 文档 https://docs.docker.com/compose/