Docker + Compose 实战:把本地项目一键搬到云服务器,附完整配置
"在我电脑上能跑"的终结篇。
做毕设、接外包、搭个人项目,最怕的就是:本地跑得好好的,丢到服务器上就各种报错。这篇文章把我这两年用 Docker 把 Node / 前端项目搬上云服务器的完整流程整理出来------从 Dockerfile 瘦身、Compose 编排、数据持久化,到高频报错排查和一次性部署,全部给可复制的命令。
一、为什么我不再手动装环境
刚学的时候我也是 SSH 上去手动 apt install、npm install、改配置文件,每次换机器都要重来一遍,还经常因为 Node 版本不一样出现诡异 bug。Docker 解决的其实就是"环境一致性"这个老大难。
| 对比项 | 手动装环境 | 用 Docker |
|---|---|---|
| 新机器上手 | 装运行时、装数据库、配环境变量,半天起步 | 装好 Docker 跑一条 docker compose up -d |
| 版本不一致 | 同学装 Node 18、你装 20,行为不一样 | 镜像锁死版本,人人一致 |
| 迁移 / 重装 | 重来一遍,还容易漏装依赖 | 镜像直接带走,秒级还原 |
| 多项目隔离 | 全局装包互相污染 | 每个容器独立环境,互不干扰 |
一句话:把"环境"也当成代码管理起来。
二、Dockerfile 怎么写才不臃肿
我踩过最大的坑是第一次打出来的镜像 1.2GB,传服务器传了十分钟。核心是两招:多阶段构建 + .dockerignore。
以 Node 项目为例,多阶段构建把"编译期依赖"和"运行期依赖"分开:
dockerfile
# 阶段一:构建阶段(这里装的是 devDependencies,只为打包)
FROM node:18-alpine AS build
WORKDIR /app
# 先拷依赖清单,利用缓存,改代码不会每次都重装
COPY package*.json ./
RUN npm ci --registry=https://registry.npmmirror.com
COPY . .
RUN npm run build
# 阶段二:运行阶段(只带运行需要的产物,体积骤降)
FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV=production
# 只从构建阶段拷运行产物和依赖,构建阶段的 node_modules 被丢弃
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
# 解决容器内中文乱码 / 时区问题
ENV LANG=C.UTF-8
ENV TZ=Asia/Shanghai
EXPOSE 3000
CMD ["node", "dist/main.js"]
.dockerignore 一定要写,否则 node_modules、.git 会被一起打进镜像,体积爆炸:
gitignore
node_modules
npm-debug.log
.git
.gitignore
.env
Dockerfile
docker-compose.yml
dist
README.md
瘦身要点小结:基础镜像用 alpine;COPY package*.json 在 COPY . . 之前以命中缓存;能多阶段的绝不把构建工具带进运行镜像;定期 docker image prune -f 清理悬空镜像。
三、用 Compose 编排 web + db + 反向代理
单个容器用 docker run,但真实项目至少要有「应用 + 数据库 + 入口代理」。Compose 一个文件就把它们编排好(新版本已经不需要 version 字段):
yaml
services:
web:
build: . # 用当前目录的 Dockerfile 构建
ports:
- "3000:3000" # 开发时可临时暴露,生产建议只走 nginx
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:app@db:5432/appdb # 注意主机名是服务名 db
depends_on:
db:
condition: service_healthy # 等数据库真正能连上再启动 web
restart: unless-stopped
networks:
- app-net
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=appdb
volumes:
- db-data:/var/lib/postgresql/data # 数据落盘,容器删了数据还在
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- app-net
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- web
restart: unless-stopped
networks:
- app-net
volumes:
db-data: # 命名卷,数据持久化靠它
networks:
app-net:
driver: bridge
容器之间用服务名(db、web)互访,不用写 IP。这样本地和服务器跑的是同一套编排,差别只在配置文件。
四、数据卷持久化与环境变量管理
持久化 :数据库、上传文件必须挂卷,否则容器一删数据全没。命名卷(上面 db-data)由 Docker 管理最省心;自己映射宿主机目录则用 ./uploads:/app/uploads。
环境变量 :千万别把密码写进 docker-compose.yml 然后提交到仓库。正确做法是用 .env 文件 + environment 引用:
bash
# .env(记得加进 .gitignore,绝不入库)
POSTGRES_PASSWORD=你的强密码
DB_NAME=appdb
yaml
# docker-compose.yml 里这样引用
services:
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${DB_NAME}
docker compose 默认会自动读取同目录的 .env。上线前用 docker compose config 检查一下变量是否都替换正确。
五、常用命令速查表
| 命令 | 作用 |
|---|---|
docker compose up -d |
后台启动全部服务 |
docker compose ps |
查看各容器状态 |
docker compose logs -f web |
实时跟踪 web 服务日志 |
docker compose build --no-cache |
不用缓存重新构建(改了 Dockerfile 用它) |
docker compose down |
停止并删除容器/网络(不删卷) |
docker compose down -v |
连数据卷一起删(危险,慎用) |
docker compose exec db psql -U app |
进数据库容器执行命令 |
docker image prune -f |
清理悬空镜像释放空间 |
docker stats |
看各容器实时资源占用 |
六、高频报错逐个拆
报错 1:端口被占用 (Bind for 0.0.0.0:3000 failed)
先查谁占了端口:sudo lsof -i :3000 或 netstat -tlnp | grep 3000,杀掉对应进程,或改 ports 映射成 "3001:3000"。
报错 2:容器起来就退出 (Exited (0) / 不断重启)
docker compose logs web 看真实报错;多半是启动命令瞬间执行完就退出了(比如 CMD 指向了一个一次性的脚本)。后台服务必须用阻塞进程(如 node 常驻)。
报错 3:Permission denied
宿主机挂卷后容器内写不进,通常是卷目录属主是 root 而容器用非 root 用户。解决:在 Dockerfile 里 RUN chown -R node:node /app 并 USER node;CentOS/RHEL 上挂卷还要加 SELinux 标签 :z。
报错 4:时区不对 (日志时间差 8 小时)
镜像里没设时区。在 Dockerfile 加 ENV TZ=Asia/Shanghai,alpine 可补 RUN apk add --no-cache tzdata。
报错 5:中文乱码
容器默认 locale 不是 UTF-8。加 ENV LANG=C.UTF-8,并确保源码文件本身是 UTF-8 编码。
报错 6:卷里数据消失了
八成是用了 docker compose down -v,或者改了卷名导致映射到新卷。重要的卷用命名卷并远离 -v 参数。
报错 7:healthcheck 一直 unhealthy
docker inspect <容器> | grep Health 看检测输出。常见原因:检测命令写错、数据库还没初始化完(调大 interval/retries)、检测依赖网络但容器没联网。
七、从本地到服务器的一次性部署流程
我在本地调通后,上服务器的流程基本固定成下面几步:
bash
# 1)服务器装 Docker(Ubuntu 示例)
curl -fsSL https://get.daocloud.io/docker | bash
sudo systemctl enable --now docker
# 2)把项目文件传上去(.env 和 docker-compose.yml 一起,但 .env 不进 git)
scp -r ./project root@你的服务器IP:/home/app/
scp ./.env root@你的服务器IP:/home/app/project/
# 3)登录服务器启动
ssh root@你的服务器IP
cd /home/app/project
docker compose up -d --build
docker compose ps # 确认三个服务都是 healthy/up
docker compose logs -f # 看启动有没有报错
收个尾:本地你只需要维护好 Dockerfile、docker-compose.yml、.env 三个文件,上任何一台装了 Docker 的机器都能一条命令把整套环境拉起来------今晚就把你的毕设项目按这套流程打包一遍,下次换电脑或交部署文档时,你直接把这三个文件丢过去就行。