前言作为一个只写过前端代码、从未接触过服务器部署的开发者当我第一次需要把本地的三维编辑器支持爆炸图编辑、三维组态、设备空间分析部署到联通云服务器上时面对Docker、容器、安全上下文这些概念完全是懵的。这篇文章记录了我从零开始把项目成功部署到云端的完整过程以及踩过的那些让人抓狂的坑。一、项目架构与部署全景图在深入代码前我们先建立整体认知。我们的三维编辑器系统包含这些部分关键概念我们并不是直接把代码放到服务器上运行而是使用Docker创建一个集装箱容器在里面配置好所有运行环境然后把集装箱放到服务器上。二、核心文件详解每个文件都在做什么1. Dockerfile —— 集装箱的装修图纸这个文件告诉 Docker我要创建一个包含 Apache、PHP 和各种配置的镜像。逐行解释# 基于官方 PHP 8.2 Apache 镜像就像选择毛坯房户型 FROM php:8.2-apache # 安装系统依赖zip 扩展用于文件压缩上传 RUN apt-get update apt-get install -y \ libzip-dev zip unzip \ docker-php-ext-install zip \ rm -rf /var/lib/apt/lists/* # 清理缓存减小镜像体积 # 启用 Apache 的四个关键模块 # rewrite: URL 重写支持前端路由 # headers: HTTP 头管理CORS 跨域需要 # mime: 文件类型识别确保 .js 被识别为 JS 而非文本 # deflate: Gzip 压缩让 5MB 的 JS 文件传输更快 RUN a2enmod rewrite headers mime deflate # --- Apache 站点配置核心--- # 这里使用 HERE 文档APACHECONF批量写入配置文件 RUN cat /etc/apache2/sites-available/000-default.conf APACHECONF VirtualHost *:80 DocumentRoot /var/www/html # 网站根目录 DirectoryIndex cc_editor.html # 默认首页 # 大文件传输超时设置我们的 JS 有 5MB Timeout 300 KeepAlive On Directory /var/www/html AllowOverride All # 允许 .htaccess 覆盖配置 Require all granted # 允许所有 IP 访问 # --- MIME 类型修复坑点--- # 确保浏览器知道 .js 是脚本.babylon 是 JSON AddType application/javascript .js .mjs AddType application/json .json .babylon # --- 跨域支持CORS--- # 允许任何域名调用我们的 API开发阶段方便 Header set Access-Control-Allow-Origin * # --- Gzip 压缩性能优化--- IfModule mod_deflate.c AddOutputFilterByType DEFLATE application/javascript DeflateCompressionLevel 6 # 压缩级别 1-96 是平衡值 /IfModule /Directory /VirtualHost APACHECONF # --- PHP 配置调整 --- # 解决上传文件大小限制默认只有 2M我们改成 200M 支持模型上传 RUN cat /usr/local/etc/php/conf.d/custom.ini PHPINI upload_max_filesize 200M post_max_size 200M max_execution_time 300 # 脚本最长运行时间秒 memory_limit 256M # PHP 可用内存 PHPINI # 复制当前目录所有文件到容器的 /var/www/html网站根目录 WORKDIR /var/www/html COPY . /var/www/html/ # 设置文件权限Linux 权限管理 # www-data 是 Apache 的默认用户必须让他有读写权限 RUN chown -R www-data:www-data /var/www/html \ find /var/www/html -type d -exec chmod 755 {} \; \ find /var/www/html -type f -exec chmod 644 {} \; \ chmod -R 775 /var/www/html/Resources # 资源目录需要写入权限 # 暴露 80 端口HTTP 默认端口 EXPOSE 80 CMD [apache2-foreground] # 容器启动时运行 Apache2. deploy.sh —— 一键部署脚本重点详解这是我最困惑又最重要的文件。逐行解释#!/bin/bash # 上面这行叫 Shebang告诉系统用 /bin/bash 执行此脚本 # set -e 表示如果任何命令执行失败返回非0立即停止脚本 # 防止带病继续执行造成更难排查的问题 set -e # 定义变量避免重复写方便修改 IMAGE_NAMEcc_dist # Docker 镜像名称类似类名 CONTAINER_NAMEcc_dist_app # 容器实例名称类似对象名 HOST_PORT30513 # 服务器对外端口用户访问用 CONTAINER_PORT80 # 容器内部端口Apache 监听 # cd $(dirname $0) 是进入脚本所在目录的安全写法 # $0 是脚本本身路径dirname 取目录解决在哪里执行脚本的路径问题 cd $(dirname $0) # 定义 start 函数封装启动逻辑 start() { echo 正在构建镜像 ${IMAGE_NAME}... # docker build -t 标签 . 表示用当前目录 Dockerfile 构建镜像 # . 是构建上下文COPY 命令会复制这个目录的文件 docker build -t ${IMAGE_NAME} . # 检查是否已有同名容器在运行 # docker ps -a 列出所有容器包括停止的 # --format {{.Names}} 只输出名字 # grep -q 安静模式匹配匹配到返回0真 if docker ps -a --format {{.Names}} | grep -q ^${CONTAINER_NAME}$; then echo 已存在容器 ${CONTAINER_NAME}先删除... # 2/dev/null 把错误输出扔掉比如容器不存在时的报错 # || true 确保即使删除失败脚本也不停止set -e 会杀掉脚本 docker rm -f ${CONTAINER_NAME} 2/dev/null || true fi echo 正在启动容器映射端口 ${HOST_PORT} - ${CONTAINER_PORT}... # docker run -d 后台运行daemon # --name 给容器起名字方便后续管理 # -p 端口映射主机端口:容器端口 docker run -d \ --name ${CONTAINER_NAME} \ -p ${HOST_PORT}:${CONTAINER_PORT} \ ${IMAGE_NAME} echo 启动完成。访问: http://服务器IP:${HOST_PORT} } # 定义 stop 函数优雅停机 stop() { if docker ps -a --format {{.Names}} | grep -q ^${CONTAINER_NAME}$; then echo 正在停止并删除容器... docker stop ${CONTAINER_NAME} 2/dev/null || true # 先停止进程 docker rm ${CONTAINER_NAME} 2/dev/null || true # 再删除容器 echo 已停止。 else echo 容器不存在无需停止。 fi } # case 语句处理命令行参数$1 是第一个参数 case ${1:-} in # ${1:-} 表示如果 $1 未定义用空字符串 start) start # 调用上面定义的 start 函数 ;; stop) stop ;; *) # * 通配符匹配任何其他输入 echo 用法: $0 {start|stop} echo start - 构建并启动 echo stop - 停止服务 exit 1 # 返回错误码 ;; esac使用方式# 赋予执行权限第一次使用需要 chmod x deploy.sh # 启动服务 ./deploy.sh start # 停止服务 ./deploy.sh stop3. file_manager.php —— 资源管理的大脑这是一个API 接口文件供前端 Vue 调用实现类似资源管理器的功能get-tree: 获取目录树左侧文件树展示get-files: 获取文件列表带过滤比如只看 .glb 模型upload-file: 上传模型/贴图支持大文件delete-file: 删除资源get-text-file-content: 读取场景配置文件.e3d/.c3d安全设计validatePath()函数防止目录遍历攻击比如传入../../../etc/passwd试图读取系统文件限制BASE_DIR为/Resources目录所有操作只能在这个沙盒内进行白名单校验文件后缀只允许jpg/png/glb/gltf等4. diag.html —— 部署诊断神器部署时如果页面白屏打开这个文件能看到Environment: 是否安全上下文、WebGL 支持、UUID 可用性Critical Files: 检查核心 JS 文件是否正确加载大小检测防止传输损坏Resource Directories: 测试 PHP 接口是否连通External CDN: 检查能否访问腾讯图标库内网环境往往不通三、Docker 到底是什么为什么要用它通俗理解想象你要搬家部署应用传统方式把家具代码搬到新家服务器然后现场组装家具、接水管、接电线安装 Apache、PHP、配置环境。每次搬家都要重新组装而且新家的螺丝孔位可能不一样服务器系统差异。Docker 方式把整个装修好的房子容器连地基一起拔起来用吊车运到新家接通水电端口映射即可入住。技术定义Docker 使用容器化技术将应用及其依赖操作系统、Web 服务器、运行时打包成一个镜像Image。镜像运行时成为容器Container。核心优势环境一致性本地开发的容器放到联通云上一模一样不会出现我本地明明是好的这种问题隔离性容器内的 PHP 崩溃不会影响服务器上的其他服务如 MySQL轻量级不像虚拟机需要完整操作系统容器共享主机内核启动秒级部署流程图解本地开发 → 构建镜像docker build→ 保存/传输 → 联通云服务器 ↓ 用户浏览器 ← 端口映射30513← 运行容器docker run← 加载镜像四、踩坑实录那些让人崩溃的问题坑 1crypto.randomUUID 在云端失效现象本地http://localhost:8080运行完美部署到联通云http://125.34.90.250:30513后页面白屏控制台报错crypto.randomUUID is not a function原因浏览器安全上下文Secure Context浏览器规定localhost/127.0.0.1永远信任所有高级 API如 crypto、摄像头、地理位置可用192.168.x.x内网 IP或公网 IP不信任必须使用 HTTPS 才能使用 crypto API解决方案Polyfill 降级在页面head最顶部插入这段代码script // 如果浏览器不支持 randomUUID非安全上下文手动实现一个 if (typeof crypto.randomUUID ! function) { crypto.randomUUID function() { // 使用 crypto.getRandomValues这个 API 在任何环境都可用 // 生成符合 RFC4122 标准的 UUID v4 return 10000000-1000-4000-8000-100000000000.replace(/[018]/g, function(c) { return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] 15 c / 4).toString(16); }); }; } /script原理crypto.getRandomValues不受安全上下文限制我们用它生成随机数手动拼接成 UUID 格式保证在低版本浏览器或非 HTTPS 环境下也能生成唯一 ID。坑 2TDesign 图标显示为方框或空白现象按钮、菜单的图标不显示但文字正常。原因TDesign 默认使用图标字体Icon Font通过https://tdesign.gtimg.com/...CDN 加载。联通云服务器如果是内网环境或防火墙限制无法访问腾讯 CDN导致字体加载失败。两种写法的区别写法 A字符串方式 - 原写法t-icon :namevisible ? chevron-left : chevron-right /原理组件内部根据字符串name动态生成i classt-icon-chevron-left依赖 CSS 字体文件中的矢量图标问题CSS 里定义了font-face { src: url(https://tdesign.gtimg.com/...)CDN 失效则图标失效写法 B组件方式 - 解决方案script setup import { ChevronLeftIcon, ChevronRightIcon } from tdesign-icons-vue-next; /script template component :isvisible ? ChevronLeftIcon : ChevronRightIcon / /template原理直接引入** SVG 组件**图标作为 Vue 组件编译进项目不依赖外部 CDN优势离线可用首屏渲染更快不额外请求字体文件建议内网/政企项目永远使用写法 B避免外部依赖。坑 3localhost 能访问IP 地址访问失败现象http://localhost/cc_dist/cc_editor.htmlWebGL 正常UUID 正常功能完整http://192.168.90.227/cc_dist/cc_editor.htmlUUID 报错某些 API 被禁用原因同样是Secure Context策略。浏览器将localhost视为安全开发环境给予特权而任何 IP 地址包括内网 IP被视为潜在风险环境限制敏感 API。完整权限对比表表格访问方式Secure Contextcrypto.randomUUIDWebGL摄像头http://localhost✅ 是✅ 可用✅✅http://192.168.x.x❌ 否❌ 禁用✅❌http://公网IP❌ 否❌ 禁用✅❌https://任意✅ 是✅ 可用✅✅启示生产环境务必配置 HTTPS即使使用自签名证书否则现代 Web API 功能受限。五、完整部署流程总结如果你是第一次部署按这个顺序操作# 1. 本地构建前端确保生成 dist 目录 npm run build # 2. 将构建产物上传到联通云服务器使用 scp 或 FTP scp -r dist/* root125.34.90.250:/data/yuzhexu/cc_dist/ # 3. SSH 登录服务器 ssh root125.34.90.250 cd /data/yuzhexu/cc_dist # 4. 执行部署使用我们详解过的脚本 ./deploy.sh start # 5. 验证部署 # 浏览器访问http://125.34.90.250:30513/diag.html # 确认所有检查项为 OK # 6. 正式使用 # 访问http://125.34.90.250:30513/cc_editor.html六、结语给新手的建议Docker 不是魔法它只是把配置环境这个步骤提前到本地确保一次构建到处运行。理解容器 进程 文件系统隔离就不再有神秘感。浏览器安全策略是道坎localhost和IP 地址的差异、HTTP和HTTPS的差异不是 Bug是浏览器的安全设计。开发时可以用localhost上线后必须规划 HTTPS。离线优先如果你的系统在专网/内网运行如煤矿井下系统永远不要依赖外部 CDN字体、图标、JS 库全部使用本地资源或 npm 包引入。诊断工具先行像diag.html这样的诊断页面是部署的听诊器先确保基础资源加载、MIME 类型、CORS 配置正确再排查业务逻辑。从零到一的部署最难的是理解全流程。希望这篇博客能成为你部署路上的新手村攻略祝你的三维编辑器顺利上线