这次我们来看一套非常实在的部署流程:本地代码通过 FTP 传到服务器,再用 Docker 在服务器上跑起来。很多小团队、独立开发者现在还不愿意上完整的 CI/CD,项目也不大,用 FTP 手动上传、SSH 登录、Docker 构建,是最快的上线方式。这套流程不需要额外搭代码仓库、不需要写流水线,只要能装 Docker、能传文件,服务就能跑。
这篇文章会把整条链路拆开:FTP 工具怎么连接、上传哪些文件、Dockerfile 怎么写、镜像怎么构建、容器怎么运行、端口怎么映射、日志怎么看,以及常见的端口冲突、权限问题和安全配置。整个过程以手动部署为主,适合个人项目和中小型业务的快速发布。如果你已经有 K8s 或 GitLab Runner,那这篇文章的价值不大。
先说清楚几个重点:Docker 负责服务器端的应用隔离和运行,FTP 解决本地到服务器的文件传输,最后通过浏览器或 curl 访问服务验证容器是否正常。整体门槛不高,一台 Linux 服务器、本机装一个 FTP 客户端、服务器装好 Docker 就能操作。下面按实际部署顺序展开。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 部署目标 | 将本地代码上传到服务器并通过 Docker 运行 |
| 核心工具 | FTP/SFTP 客户端 + Docker Engine + Linux 服务器 |
| 主要流程 | 本地打包代码 -> FTP 上传 -> SSH 登录 -> 编写 Dockerfile -> 构建镜像 -> 运行容器 |
| 操作系统 | 服务器建议 Ubuntu/Debian/CentOS;本地任意系统均可 |
| Docker 要求 | Docker Engine 20.10+,Docker Compose 可选 |
| FTP 要求 | 支持 FTP/SFTP 的客户端,推荐 FileZilla、WinSCP、FinalShell |
| 是否支持 API | Docker 守护进程可开启 TCP API,默认关闭,生产环境不建议直接暴露 |
| 是否支持批量任务 | 支持,通过 FTP 批量目录上传 + docker compose 批量启动多个服务 |
| 硬件门槛 | 最低 1 核 1G 内存,实际取决于应用类型和并发量 |
| 适合场景 | 小型 Web 服务、个人项目、临时交付、无 CI/CD 环境的快速发布 |
这套方案本质上是“手动发布流程”的标准化:FTP 只负责传输,Docker 负责环境一致性。好处是服务器不需要安装 Node、Python 或 Java 的运行时,所有依赖都打进镜像里;坏处是每次更新都需要手动上传文件并重新构建容器,比 Git 触发自动部署多两步操作。
2. 适用场景与使用边界
Docker + FTP 的方式适合以下几类场景。
第一,个人博客、小型 API、管理后台这类轻量应用。没有专职运维,也不想引入复杂的 CI/CD 工具链。把代码传到服务器,构建镜像,一条 docker run 命令就能启动,回滚也简单:重新跑一个旧版本镜像即可。
第二,客户短期项目或临时演示环境。对方只需要一个可访问的 HTTP 服务,不需要完整的部署平台。FTP 上传代码后,用 Docker 隔离运行,对宿主机影响小,删除也方便。
第三,已有 Docker 环境但缺少 Git 服务器的小团队。团队成员通过 FTP 把代码包传到固定目录,由负责人或脚本统一触发构建,避免多人直接在生产服务器上改源码。
但同时也要看清边界。
这套流程不适合需要频繁发布、多环境管理和细粒度权限控制的场景。FTP 上传是明文传输(普通 FTP),在公网环境下容易被嗅探,建议使用 SFTP 或 FTPS。FTP 没有版本管理能力,多个开发者同时上传容易互相覆盖,也不适合多人协作的大项目。
Docker 本身不解决代码正确性问题。镜像构建成功不代表业务正常,容器启动后还要做健康检查、看日志、验证接口。另外,不要为了省事把 Docker 守护进程的 TCP 端口直接暴露到公网,这等于把一个无鉴权的管理接口交给互联网,风险非常高。
3. 环境准备与前置条件
在开始部署前,先确认本机和服务器的基础环境。
3.1 服务器端要求
服务器需要满足以下条件:
- Linux 操作系统,推荐 Ubuntu 20.04+、Debian 12、CentOS 7/8 或兼容系统。
- 已安装 Docker Engine,可以通过
docker --version查看。 - 可用的 SSH 登录权限,最好使用 root 或具有 sudo 权限的用户。
- 对外开放 FTP/SFTP 所需的端口(21 或 22),以及应用对外服务的端口(如 8080)。
- 磁盘空间至少 5G 以上,用于存放源码、构建缓存和镜像。
如果服务器还没装 Docker,简单方式如下(以 Ubuntu 为例,实际版本以官方文档为准):
# 更新软件源 sudo apt update # 安装 Docker 依赖 sudo apt install -y ca-certificates curl # 导入 Docker 官方 GPG 密钥 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加软件源 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装 Docker sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io # 验证 sudo docker --versionCentOS/RHEL 系列通常使用 yum,命令结构类似,但软件源不同,需要参考对应系统的 Docker 官方安装文档。如果你是 CentOS,建议先确认内核版本和 SELinux 状态,再安装 docker-ce。
3.2 本地端要求
本地不需要安装 Docker,只要有 FTP 客户端和 SSH 终端即可。
推荐的 FTP/SFTP 客户端:
- FileZilla:跨平台,界面直观,支持 FTP、SFTP、FTPS。
- WinSCP:Windows 下常用,支持批处理和目录同步。
- FinalShell:集 SSH、FTP 于一体,适合 Java/Python 后端调试。
SSH 终端可以使用系统自带的终端,也可以使用 Xshell、MobaXterm、Termius 等工具。Windows 用户直接使用 PowerShell 或 Windows Terminal 也可以。
3.3 检查网络与端口
部署前先确认服务器端口是否可达。例如要访问 8080 端口,可以在本地执行:
telnet 服务器IP 8080如果端口不通,检查云服务商的安全组规则和服务器防火墙:
# 查看防火墙状态 sudo ufw status # 或者 sudo firewall-cmd --list-allFTP 连接失败通常也是端口问题:FTP 主动模式需要服务器开放 20/21 端口,被动模式需要开放服务器的被动端口范围,这往往是新手最容易踩的坑。
4. FTP 上传本地代码到服务器
代码从本地到服务器,重点不是简单拖动文件,而是知道传什么、传到哪里、怎么传。
4.1 创建远程目录
先通过 SSH 登录服务器,创建项目目录:
# 登录服务器 ssh root@服务器IP # 创建目录,例如 /opt/app/my-web mkdir -p /opt/app/my-web把项目放在/opt/app下是常见做法,权限清晰、不会和系统文件混在一起。如果使用非 root 用户,需要确保该用户对目录有写权限:
sudo chown -R $USER:$USER /opt/app/my-web4.2 选择上传方式和连接
打开 FileZilla 或 WinSCP,连接参数如下:
| 参数 | 值 |
|---|---|
| 协议 | SFTP(推荐)或 FTP |
| 主机 | 服务器公网 IP |
| 端口 | SFTP 默认 22,FTP 默认 21 |
| 用户名 | root 或其他有权限的用户 |
| 密码 | 对应登录密码或密钥 |
使用 SFTP 的优势是数据走 SSH 加密通道,即使密码被截获,数据流也是密文。如果服务器只开放了 FTP 端口,也可以使用 FTPS(FTP over SSL)。
4.3 上传前排除无用文件
直接把整个项目目录拖上去是最容易出问题的做法。node_modules、__pycache__、.git、*.log、dist等目录体积大且不是源码,上传后既浪费时间,也可能让 Docker 构建上下文变得巨大。
上传前先在工作目录写一个.gitignore或使用客户端过滤规则。Windows 下可以用 WinSCP 的“跳过列表”,FileZilla 可以在站点管理器中设置“过滤器”。
正确的上传内容是:源码文件、配置文件、依赖描述文件、Dockerfile、环境变量示例等。
4.4 上传产物验证
上传完成后,在 SSH 中确认目录结构:
cd /opt/app/my-web ls -la # 查看文件数量 find . -type f | wc -l如果上传的是压缩包,可以只传一个 tar.gz,然后在服务器上解压:
# 本地打包 tar -czf my-web.tar.gz --exclude=node_modules --exclude=.git . # 服务器解压 tar -xzf my-web.tar.gz这种方式在文件数量很多时比逐个上传更快,也是批量部署的常用手段。
5. 编写 Dockerfile 并构建镜像
代码到服务器后,接下来要写 Dockerfile,让 Docker 知道如何构建可运行的应用镜像。
5.1 Python 项目示例
假设是一个 Python FastAPI 或 Flask 项目,Dockerfile 可以这样写:
# 基础镜像,固定版本便于复现 FROM python:3.11-slim WORKDIR /app # 先复制依赖文件,利用 Docker 层缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再复制业务代码 COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]这个 Dockerfile 的顺序很有讲究:requirements.txt单独复制,是为了让依赖安装层保持缓存。如果只改了业务代码,重新构建时不会重新跑pip install,能显著缩短构建时间。
5.2 Node.js 项目示例
如果是前端或 Node 服务,Dockerfile 类似:
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . EXPOSE 3000 CMD ["node", "server.js"]如果项目需要先构建前端产物,可以拆成多阶段构建,减小最终镜像体积。
5.3 添加 .dockerignore
Docker 构建时会发送整个上下文,因此一定要在项目根目录放.dockerignore,避免把无关文件复制进镜像:
node_modules .git *.md *.log .DS_Store __pycache__ dist .env注意,.dockerignore只作用于构建上下文,不是服务器上的运行目录。如果应用启动时还需要.env文件,建议通过--env-file或挂载方式注入,而不是打进镜像。
5.4 构建镜像
在项目目录执行:
cd /opt/app/my-web # 构建镜像,使用标签 app:v1 sudo docker build -t my-web:v1 .构建期间如果网络不好,可以给 Docker 配置镜像加速。编辑/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.m.daocloud.io" ] }然后重启 Docker:
sudo systemctl restart docker构建完成后,用docker images查看镜像:
sudo docker images如果看到my-web:v1,说明构建成功。如果看不到,需要倒回去看 Dockerfile 的每一步日志,常见原因是依赖下载失败或复制路径不对。
6. 运行 Docker 容器并暴露服务
镜像构建成功只是第一步,真正要让外部访问,必须把容器端口映射到宿主机,并保证业务进程前台运行。
6.1 直接运行容器
sudo docker run -d \ --name my-web-container \ -p 8080:8000 \ --restart unless-stopped \ my-web:v1参数说明:
-d:后台运行。--name:给容器命名,便于后续操作。-p 8080:8000:宿主机 8080 端口映射到容器内 8000 端口。--restart unless-stopped:容器意外退出时自动重启,适合服务常驻。
如果你的 Dockerfile 里 EXPOSE 是 3000,就把映射改成-p 8080:3000。具体端口由你的项目决定,不要照抄。
6.2 查看容器状态
sudo docker ps看到Up状态说明容器在运行。再看日志:
sudo docker logs -f my-web-container日志是排查启动问题的最核心手段。如果容器启动后立刻退出,先看日志:
sudo docker logs --tail 100 my-web-container常见的退出原因有:应用监听端口写错、缺少环境变量、磁盘权限不足、启动命令找不到。
6.3 访问验证
本地浏览器访问:
http://服务器IP:8080或者在 SSH 上用 curl 验证:
curl -I http://127.0.0.1:8080如果返回200 OK,说明 Docker 容器已经正常工作。如果超时,先确认服务是否监听在0.0.0.0。很多框架默认监听127.0.0.1,在容器内只接受本机回环,外部访问不到,需要把监听地址改成0.0.0.0。
6.4 使用 Docker Compose 管理多服务
如果项目包含 Nginx、Redis、MySQL 等多个服务,建议写docker-compose.yml管理:
version: "3" services: web: build: . ports: - "8080:8000" environment: - MODE=production restart: unless-stopped redis: image: redis:7-alpine restart: unless-stopped然后启动:
sudo docker compose up -d --buildDocker Compose 会让多个容器用同一个网络互相访问,免去手动配置--link。更重要的是,它可以处理“更新代码后重建并重启”的批量操作,适合手动部署升级。
7. 接口 API 与批量任务
虽然 FTP 上传是手动操作,但部署流程中的“接口”和“批量”仍然值得考虑。这里讲两个层面:Docker 服务本身的 API,以及 FTP/构建的批量处理思路。
7.1 Docker 守护进程 API
Docker 守护进程默认只监听本地 Unix Socket,不对外开放 HTTP API。如果确实需要远程管理,可以在 Docker daemon 配置中增加 TCP 监听,比如:
{ "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"] }但危险在于:2375 端口无加密、无鉴权,一旦暴露到公网,别人就可以直接操作你的 Docker,创建任意容器。生产环境千万不要这样暴露。如果只是本地测试,可以临时打开,用完立即关闭;更稳妥的做法是通过 SSH 隧道访问远程 Docker API:
ssh -L 2375:127.0.0.1:2375 user@服务器IP此时本地可以访问http://127.0.0.1:2375/version:
curl http://127.0.0.1:2375/version如果能看到 Docker 版本信息,说明 API 通了。接下来可以调用 Docker API 列出容器:
curl http://127.0.0.1:2375/containers/json这只是演示 API 用法,不会在你没有配置的情况下生效。实际项目中,用 Docker SDK for Python、docker CLI 或 docker-compose 都比直接裸调用 API 更安全。
7.2 FTP 批量上传
FTP 工具本身就支持批量。FileZilla 可以把多个站点保存成一个队列,WinSCP 支持命令行同步目录。脚本化场景可以使用lftp:
# 使用 lftp 同步本地目录到远程目录 lftp -u username,password -e "mirror -R ./local_dir /remote_dir; quit" sftp://服务器IP这个命令会将本地目录整个镜像到远程目录,删除本地不存在的文件需要用--delete参数。用于定时发布时注意,--delete可能误删远程文件,生产环境建议先备份。
7.3 批量构建与启动
服务器上如果有多个应用目录,可以写一个简单循环批量构建:
for dir in /opt/app/*/; do cd "$dir" || continue if [ -f docker-compose.yml ]; then sudo docker compose up -d --build fi done这种脚本适合统一管理多个独立项目,但需要考虑失败重试和日志采集。一次循环里如果有某个服务构建失败,应输出错误并记录,而不是整个脚本直接崩掉。可以把执行结果重定向到日志文件:
for dir in /opt/app/*/; do cd "$dir" || continue echo "=== Building $dir ===" >> /var/log/deploy.log sudo docker compose up -d --build >> /var/log/deploy.log 2>&1 done注意,docker compose在不同系统上可能是docker-compose(带连字符),使用前先确认版本。
8. 资源占用与性能观察
部署完成后,要观察服务器的资源使用情况,判断是否有必要优化。
8.1 查看容器资源占用
使用docker stats可以动态查看各容器的 CPU、内存、网络和磁盘 I/O:
sudo docker stats --all这个命令类似 Linux 的 top,但显示的是每个容器的资源配额。长时间运行后,如果容器内存持续增长,说明可能存在内存泄漏,需要考虑重启策略或应用级优化。
查看磁盘占用:
sudo df -h sudo du -sh /opt/app/*Docker 的构建缓存和悬空镜像也会占磁盘空间。定期清理无用的镜像和构建缓存:
# 查看悬空镜像 sudo docker images -f dangling=true # 清理悬空镜像 sudo docker image prune # 清理所有未使用的镜像 sudo docker image prune -a如果频繁更新代码并重新构建镜像,旧镜像会越来越多,建议定期执行docker system prune,但要注意这个命令会移除停止的容器和未使用的网络,不要在生产高峰期随意执行。
8.2 限制容器资源
为了防止某个应用把服务器资源吃满,可以在docker run时限制内存和 CPU:
sudo docker run -d \ --name my-web-container \ -p 8080:8000 \ --memory 512m \ --cpus 1.0 \ my-web:v1--memory 512m限制容器最大使用 512MB 内存,--cpus 1.0限制最多使用 1 个 CPU 核心。对于负载敏感的服务,可以设置 swap 限制--memory-swap 1g。但这些数值必须根据实际应用和服务器配置调整,不能随意照搬。
8.3 构建性能优化
镜像构建速度会影响发布频率。几个实用技巧:
- 优先使用
COPY requirements.txt而不是COPY . .,让依赖层复用缓存。 - 合并
RUN apt-get update && apt-get install -y ...,减少镜像层数。 - 使用
--no-cache或无需要时不要强制重新拉取基础镜像。 - 构建时指定
--progress=plain可以看到完整日志,便于定位卡住的位置。
sudo docker build --progress=plain -t my-web:v2 .9. 常见问题与排查方法
9.1 排错表格
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| FTP 连接失败 | 端口未开放、被动模式范围限制 | 检查安全组和防火墙 | 开放 20/21 端口或被动端口范围 |
| 文件上传后中文乱码 | 传输模式为 ASCII | 切换为二进制方式 | FileZilla 默认自动,可手动切换二进制模式 |
| docker: Permission denied | 当前用户不在 docker 组 | sudo groupadd docker && sudo usermod -aG docker $USER | 重新登录或重启会话 |
| 端口冲突 | 8080 已被占用 | sudo lsof -i:8080或sudo netstat -tunlp | grep 8080 | 更换映射端口或停止占用进程 |
| 容器启动后立即退出 | 启动命令异常、端口监听错误 | sudo docker logs | 查看日志并修正启动命令 |
| 外部无法访问服务 | 防火墙/安全组未放行 | curl 127.0.0.1:8080测试 | 开放对应端口、确认监听地址为 0.0.0.0 |
| 镜像构建很慢 | 网络不稳定、基础镜像过大 | ping 或 curl 测试外网 | 配置镜像加速或使用 Alpine 基础镜像 |
| 容器日志过多 | 未设置日志轮转 | docker logs --tail查看大小 | 配置loggingdriver 或定期清理 |
9.2 Docker 日志轮转
日志无限增长会撑满磁盘。可以在/etc/docker/daemon.json中配置全局日志限制:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }修改后重启 Docker:
sudo systemctl restart docker这个配置对新建容器生效,已存在的容器需要重建才生效。
9.3 FTP 权限问题
上传到服务器后,文件权限可能会影响 Docker 构建。如果在构建时出现permission denied,可以检查源码目录权限:
sudo chown -R $USER:$USER /opt/app/my-web sudo chmod -R u+rwX /opt/app/my-web如果项目内有需要写入的目录,比如logs、uploads,要在 Dockerfile 中显式创建并授权:
RUN mkdir -p /app/logs && chmod 777 /app/logs生产环境建议避免777,只给运行用户最小权限。
10. 最佳实践与使用建议
FTP + Docker 的整套流程并不复杂,但稳定运行需要养成几个习惯。
10.1 传输层用 SFTP / FTPS
不要用明文 FTP 传输包含数据库配置、密钥、API Token 的文件。云平台一般都开通了 SSH 端口,直接用 SFTP 或 FTPS 替代普通 FTP,成本低、安全收益高。
10.2 上传前先构建,再跑生产
在上传到服务器之前,先在本地用 Docker 构建一次,能规避大量“服务器上编不过”的问题。本地验证通过后,再传代码。如果项目依赖太大的,可以只传源码和 Dockerfile,服务端构建时再拉取依赖。
10.3 用版本标签管理镜像
每次构建不要只覆盖latest,而是用语义化版本或时间戳:
sudo docker build -t my-web:v1.0.0 . sudo docker run -d -p 8080:8000 my-web:v1.0.0这样出了问题可以快速切回上一个版本。
10.4 健康检查要加
在 Dockerfile 中声明健康检查:
HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://127.0.0.1:8000/health || exit 1如果基础镜像中没有 curl,需要先安装。健康检查能帮助你在容器状态异常时快速发现,配合--restart unless-stopped可以自动重启不稳定进程。
10.5 环境变量与密钥分离
不要把数据库密码、Token 写进镜像。通过-e或--env-file注入:
sudo docker run -d \ --env-file .env.prod \ -p 8080:8000 \ my-web:v1.0.0同时保证.env文件不要通过 FTP 上传到公开目录,更不要提交进镜像。
10.6 发布前备份
发布前可以先备份当前运行镜像:
sudo docker commit my-web-container my-web-backup:$(date +%Y%m%d%H%M)也可以直接docker save导出镜像文件。
10.7 遵守授权与合规要求
代码可能涉及第三方开源组件、商业软件或客户数据。使用 FTP 上传时要注意目标服务器是否具备合法部署授权;如果项目涉及用户隐私数据,需要在服务器上做好访问控制和日志审计。Docker 镜像如果发布到公开仓库,要留意基础镜像许可证和业务代码保密性,不要直接 push 包含密钥的镜像。
11. 总结
FTP 上传 + Docker 部署这个组合,最实用的价值是把“手动发布”这件事变得可预期:代码文件固定传到某个目录,Dockerfile 描述运行环境,一条命令启动服务。相比直接在裸机上安装环境,Docker 让服务器更干净,也让应用之间的依赖冲突更少。
如果你想马上开始,建议先做三步:第一,用 FTP 客户端连接服务器并确认能上传文件;第二,在服务器上写一个最小 Dockerfile,构建并运行任意一个测试服务;第三,用docker logs和docker ps验证容器状态。核心链路跑通后,再把自己的真实业务镜像化,补充环境变量、端口映射和数据持久化。
最容易踩的坑是端口监听地址和防火墙设置。Docker 容器内应用必须监听0.0.0.0,宿主机端口和安全组必须放行,三者缺一不可。把这条规则记清楚,FTP + Docker 部署就不会有大问题。
后续可以把流程继续升级:换成 Git 仓库 + Webhook 自动构建,或者接入 Gitea、Jenkins,但底层仍然是一样的 Docker 镜像和容器管理。先跑通手动发布,再逐步自动化,这样每一步的风险都可控。