它是什么:把项目和运行环境一起交付
「Docker 项目分享」不是把源码压缩包丢给别人,让对方猜 Python、Node、数据库驱动该装哪个版本;而是把代码、依赖、配置和启动说明整理成可复用的镜像或构建文件。别人按步骤构建、填变量、启动,就能跑起来,且结果和你接近。这么做,是因为「我电脑上能跑」常常败在系统库、端口和依赖版本不一致。
常见交付物不止压缩包:Dockerfile 描述镜像怎么搭,镜像是构建后的成品,.env.example 是环境变量示例,数据卷说明写清哪些目录要持久化,start.sh 固定启动参数。小白常踩坑:把真实密码写进镜像或示例文件、只写「运行即可」却不写端口和数据目录、容器删掉后数据也没了。下面例子没有数据库,所以不挂数据卷;真实项目要把上传目录、数据库文件写清楚。
先确认 Docker 已安装、正在运行,当前用户能直接调用 Docker;在 Linux/macOS 或 GCP VM 终端复制执行,它会生成一套最小交付物并启动:
mkdir -p ~/docker-share-demo/html
cd ~/docker-share-demo
printf '%sn' 'Hello Docker 项目分享:环境一起交付了。' > html/index.html
printf '%sn' 'FROM nginx:alpine' 'COPY html/index.html /usr/share/nginx/html/index.html' > Dockerfile
printf '%sn' 'APP_PORT=8080' > .env.example
printf '%sn' '#!/usr/bin/env sh' 'set -e' 'APP_PORT="${APP_PORT:-8080}"' 'docker rm -f docker-share-demo 2>/dev/null || true' 'docker run -d --rm -p "$APP_PORT:80" --name docker-share-demo docker-share-demo:1.0' > start.sh
chmod +x start.sh
docker build -t docker-share-demo:1.0 .
cp .env.example .env
set -a; . ./.env; set +a
./start.sh
curl http://localhost:$APP_PORT
docker logs docker-share-demo
docker stop docker-share-demo
自动导出环境变量,是为了让 .env 里的端口真正传给启动脚本;否则改了 APP_PORT 却还开在默认端口。本机访问没反应,先查端口占用;想从外网访问 GCP VM,还要查防火墙。
在 GCP 场景里,项目可直接跑在 VM 上:把交付文件放到机器里,按脚本构建或启动。团队更推荐把镜像推到 Artifact Registry 这类镜像仓库,用 1.0、0.9 这类版本标签分发;新版本出问题时,让 VM 重新使用旧标签镜像即可回滚。坑是别只推 latest,它今天和明天可能不是同一份镜像。
解决什么问题:先避开小白最常踩的坑
做「Docker 项目分享」,别只发压缩包和一句“我电脑能跑”。小白常踩的坑是:代码给了,环境没给;本机依赖是这套版本,服务器是另一套,于是演示正常、上线报错。
Docker 把依赖版本、系统库和系统环境固定进镜像,像把项目连房间一起打包。别人拉同一个镜像,跑的是同一套底座,能减少“我电脑能跑,服务器跑不了”。镜像要写清版本,别只写“最新版”;最新版一变,坑又回来了。
这样,演示、交接、复现就是同一套流程,省下反复装库、改权限、猜端口的时间。先复制下面几行;第一条检查 Docker,报错就先装 Docker Desktop,别急着改代码。
docker --version
docker pull nginx:1.25
docker run -d --name docker-share-demo -p 8080:80 nginx:1.25
docker ps
docker logs docker-share-demo
这里固定了镜像版本、容器名和端口。端口左边是对外访问的 8080,右边是容器内部 80,别写反。端口占用就把两条启动命令里的 8080 改成 8081;容器名已存在,按后面回滚块清理。页面打不开先看状态和日志,别急着乱开权限,那只是把问题藏起来。
运维还要知道以后怎么管,交接文档至少写三件事:
- 启动命令:固定镜像标签、容器名、端口映射,谁接手都照抄。
- 日志位置:由 Docker 统一收集,写清容器名和日志查看入口,别只发截图。
- 回滚方法:保留上一个可用镜像版本,出问题时停掉新容器,用旧标签重跑。
回滚别靠记忆。新版本出问题时,用上一个可用镜像标签重跑:
docker stop docker-share-demo
docker rm docker-share-demo
docker run -d --name docker-share-demo -p 8080:80 nginx:1.24
docker logs docker-share-demo
启动命令、日志位置、回滚方法都写清,接手的人不用追问“当时怎么装的”,项目分享后才更容易长期维护。
怎么上手:在 GCP VM 上跑通第一个项目
先别急着写项目,把地基踩实。在 GCP 开一台 Ubuntu VM,点「SSH」连进终端后,先确认自己是谁、门开了哪几扇:默认只放行 tcp:22,一会儿要用 8080,得去「VPC 网络 → 防火墙」新建 allow-8080,来源 0.0.0.0/0,端口范围只写 tcp:8080,别图省事填 0-65535,再给 VM 绑上 docker-demo 网络标签。
whoami
sudo ufw status || echo "系统层没开防火墙,重点查 GCP 控制台那层"
sudo apt-get update
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
echo "装好了:请关掉 SSH 窗口重新连一次"
第一个坑:加完 docker 组不会立刻生效,不重连就一直报「权限不够」,很多人以为装失败了。重连后 docker ps 能跑通再往下走。
接着做最小示例:一张静态测试页,自己写 Dockerfile,先别碰数据库和框架。
mkdir -p ~/docker-demo && cd ~/docker-demo
cat > index.html <<'EOF'
<h1>我的第一个 Docker 项目</h1>
<p>跑在 GCP VM 的 8080 端口上</p>
EOF
cat > Dockerfile <<'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
EOF
docker build -t docker-demo:1.0 .
三行的意思:借一个现成的小 nginx、把页面拷进去、声明容器内听 80。1.0 现在就定下来,后面分享全靠它;结尾那个点别漏,它表示「在当前目录找 Dockerfile」。
mkdir -p ~/docker-demo/data
docker run -d --name demo
-p 8080:80
-e APP_MSG="你好,同事"
-v ~/docker-demo/data:/usr/share/nginx/html/data
docker-demo:1.0
docker ps
-p 8080:80 必须和防火墙开的端口一致,写成 80:80 就打不开;-e 传环境变量,这版页面还不读它,先养成习惯,换成自己写的程序直接能取,不用回头改命令;-v 是数据卷,容器删了文件仍留在 VM 里。大坑:别把卷挂到整个 /usr/share/nginx/html,空目录会盖掉刚 COPY 的 index.html,浏览器只给你 403,挂到子目录 data 就没事。
echo "浏览器打开:http://$(curl -s ifconfig.me):8080"
docker stop demo
docker start demo
docker logs demo
看到标题就成了。一直转圈九成是控制台没开 8080,不是 Docker 挂了;GCP 外部 IP 默认是临时的,重启会变,别写死进文档。stop / start 是日常启停,彻底重来才用 docker rm -f demo,data 里的东西照样在。
最后推到镜像仓库,同事或换一台服务器就能接手:
docker login
docker tag docker-demo:1.0 你的用户名/docker-demo:1.0
docker push 你的用户名/docker-demo:1.0
把「你的用户名」换成真实账号,别原样复制;也别打 latest,否则同事拉到的未必是你今天验过的那版。顺手记下版本号:
- 1.0 → 你的用户名/docker-demo:1.0,首版静态页,已在 GCP VM 验证 8080 可访问。
docker pull 你的用户名/docker-demo:1.0
docker run -d --name demo -p 8080:80 -e APP_MSG="换机也能跑" -v ~/docker-demo/data:/usr/share/nginx/html/data 你的用户名/docker-demo:1.0
换机器只要装好 Docker、开好 8080,复制上面两行即可,Dockerfile 都不用重发——这就是项目分享最省心的样子。
与替代方案对比:别把简单事做复杂
先别急着选最“专业”的方案。分享一个练手项目,目标是让对方十分钟内跑起来、看得懂、能改,不是把机房搬给他。可以先用两条命令看看体积,别凭感觉:
ls -lh
du -sh .
在 macOS、Linux 或 Git Bash 终端的项目根目录执行。第一行列文件,第二行统计当前目录大小,用来判断源码包或镜像会不会太重。坑:别把 node_modules、.venv、缓存目录一起打包,它们常带本机路径,换台电脑就报错。
- 源码压缩包:体积小、便于阅读,但依赖安装最容易出错,不适合一键演示。你电脑能跑,不代表同学能跑;README 只写“安装依赖”,对小白基本等于没写。
- 完整 VM 镜像:环境最一致,但文件大、成本和管理负担更高,适合迁移旧系统。它把系统、依赖、配置全装进去,常是几十 GB;上传慢、存储要钱,还要补丁、密码、内核升级。坑是快照可能夹带密钥和历史记录。
- 手工部署到服务器:短期看似快,长期难复现;Docker 项目分享更适合教学和协作。今天登录服务器装点东西、改点配置,过两周换机器就没人记得做过什么。把步骤写进 Dockerfile、Compose 和 README,别人才能重复跑。
- Cloud Run 等托管服务:省去服务器维护,但学习 Linux 运维细节的机会较少。平台帮你管扩容、证书和日志,适合上线;可端口、进程、权限、系统包也被藏起来了。若目标是学会项目怎么变成服务,别一上来只点按钮。
想感受 Docker 为什么适合分享,本地先跑官方最小例子:
docker --version
docker run --rm hello-world
第一行确认 Docker 是否装好,第二行拉一个很小镜像并运行;自动清理参数能避免留下垃圾容器。坑:第一次要联网,看到找不到命令就先装 Docker Desktop;不要拿生产服务器练手。简单项目先用 Docker 讲清楚,再考虑 VM 或托管服务,才是别把简单事做复杂。
最佳实践:安全、稳定、可回滚
前置条件:Ubuntu/Debian、能 sudo、已装 Docker、知道当前电脑的公网 IP;若没装,先按 Docker 官方或系统包安装,不要混装来源。示例统一放 /opt/myapp。先在测试机练,改防火墙前保留一个 SSH 窗口,执行后另开终端确认还能登录;云服务器还要在控制台安全组做同样限制。
别把密码、密钥写进镜像或仓库:镜像可被解包,.env、令牌、证书都可能泄露。运行时用环境变量、密钥管理或最小权限服务账号;生产优先密钥管理,服务账号只给必要权限。下面不显示密码地生成 .env,并阻止它被打包进镜像:
sudo mkdir -p /opt/myapp
sudo chown "$(id -un):$(id -gn)" /opt/myapp
read -r -s -p "输入数据库密码(屏幕不显示):" DB_PASS; echo
umask 077
printf 'DB_PASSWORD=%sn' "$DB_PASS" > /opt/myapp/.env
unset DB_PASS
chmod 600 /opt/myapp/.env
printf '.envn' > /opt/myapp/.dockerignore
docker pull nginx:1.27.5-alpine
坑:密码别跟在命令行参数后,会进历史记录;docker 组近似 root,别给不信任账号;.env 也要从 Git 仓库排除。
别用 latest,它可能变;固定版本并记录 digest(镜像身份证),保留构建记录、启动脚本和回滚步骤,避免今天能跑明天报错。下面生成 start.sh、rollback.sh、build.txt,并只把示例容器发布到本机 8080:
DIGEST=$(docker image inspect --format='{{index .RepoDigests 0}}' nginx:1.27.5-alpine)
case "$DIGEST" in *@sha256:*) ;; *) echo "没有拿到 digest,先执行 docker pull nginx:1.27.5-alpine"; exit 1 ;; esac
printf '%sn' "$DIGEST" > /opt/myapp/stable.txt
cp /opt/myapp/stable.txt /opt/myapp/stable.txt.bak
printf '构建时间:%sn镜像:%sn启动:/opt/myapp/start.shn回滚:/opt/myapp/rollback.shn' "$(date +%F_%H%M%S)" "$DIGEST" > /opt/myapp/build.txt
cat > /opt/myapp/start.sh <<'EOF'
#!/bin/sh
set -eu
IMAGE=$(cat /opt/myapp/stable.txt)
docker pull "$IMAGE"
docker rm -f myapp >/dev/null 2>&1 || true
docker run -d --name myapp --restart unless-stopped --env-file /opt/myapp/.env -p 127.0.0.1:8080:80 "$IMAGE"
EOF
chmod +x /opt/myapp/start.sh
cat > /opt/myapp/rollback.sh <<'EOF'
#!/bin/sh
set -eu
if [ -f /opt/myapp/stable.txt.bak ]; then
cp /opt/myapp/stable.txt.bak /opt/myapp/stable.txt
fi
/opt/myapp/start.sh
EOF
chmod +x /opt/myapp/rollback.sh
/opt/myapp/start.sh
升级前复制 stable.txt 到 stable.txt.bak,失败执行回滚并看日志,别只看“命令没报错”:
/opt/myapp/rollback.sh
docker ps -a --filter name=myapp
docker logs --tail 50 myapp
只开放必要端口、限制 SSH 来源:Web 开 80/443,SSH 只允许你的公网 IP;8080 绑定本机。Docker 会改 iptables,ufw 未必拦住已发布端口;IP 填错会锁死。每月更新系统、Docker 和基础镜像,旧版本有漏洞;apt 安装的 Docker 会随系统升级,更新前先备份、先测试:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw
docker pull nginx:1.27.5-alpine
read -r -p "输入你当前电脑的公网 IP(用于 SSH):" MY_IP
[ -n "$MY_IP" ] || { echo "IP 不能为空"; exit 1; }
sudo ufw allow from "$MY_IP" to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 8080/tcp
sudo ufw --force enable
sudo ufw status numbered
日常检查:看日志、容器状态和资源用量,能早发现启动即退出、端口冲突、磁盘满。持续日志用 5 秒自动退出;手动加 -f 时按 Ctrl+C 只是退出查看,不会停容器:
docker ps -a --filter name=myapp
docker logs --tail 100 myapp
timeout 5 docker logs --tail 100 -f myapp || true
docker stats --no-stream myapp
df -h
free -h
常见报错:
- permission denied:先用 sudo 执行;重新登录后仍不行再查账号组。
- port already allocated:查谁占用 8080,改端口或停旧容器,别反复重启。
- Cannot connect to the Docker daemon:启动 Docker 服务,并检查内存、磁盘。
- no space left on device 或 manifest unknown:先看空间、再拉固定镜像,别直接删 volumes。
sudo docker ps
sudo ss -ltnp | grep ':8080' || true
sudo systemctl status docker --no-pager
sudo systemctl start docker
docker system df
docker image prune -f
docker pull nginx:1.27.5-alpine
备份方法:至少备份 .env、脚本、版本记录、容器配置和当前镜像;有数据库或挂载目录还要做数据快照。服务器坏了或误删时才能恢复。备份复制到另一台机器或对象存储,别只放本地:
mkdir -p /opt/myapp/backup
stamp=$(date +%F_%H%M%S)
tar -czf /opt/myapp/backup/config-$stamp.tar.gz -C /opt/myapp .env start.sh rollback.sh stable.txt stable.txt.bak build.txt .dockerignore 2>/dev/null || true
docker inspect myapp > /opt/myapp/backup/inspect-$stamp.json 2>/dev/null || true
IMAGE=$(cat /opt/myapp/stable.txt)
docker save "$IMAGE" -o /opt/myapp/backup/image-$stamp.tar
ls -lh /opt/myapp/backup
常见问题(FAQ)
小白没有 GCP 账号也能练习吗?
可以先在本机 Linux 或虚拟机装 Docker,流程一致,之后再迁移到 GCP。
Docker 项目分享一定要推送镜像吗?
不一定;小项目可交付 Dockerfile 和说明,团队协作更建议用镜像仓库。
选 VM 还是 Cloud Run?
想学 Linux 运维选 VM,想少管服务器、快速上线选 Cloud Run。
分享时最容易忽略什么?
密钥、端口、数据卷和回滚;这些不写清楚,别人很难安全复现。
不想自己折腾 GCP?我们提供云平台代维护、上云迁移与账单代付服务,一对一技术支持。
