news 2026/4/23 10:44:40

Docker+Nginx+Node.js 全栈容器化部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker+Nginx+Node.js 全栈容器化部署

Docker+Nginx+Node.js 全栈容器化部署

通过 Docker Compose 统一编排 Nginx、Node.js 服务,实现前后端分离部署。前端使用 Unity WebGL 创建交互式界面,后端基于 Node.js 提供 REST API 通信。重点讲解网络配置、反向代理策略、端口映射、跨域处理等实战中遇到的问题及解决方案,为应用部署提供完整参考。
当部署到部署到服务器的时候不知道服务的ip地址,可以使用nginx代理服务,然后使用请求的时候使用路径请求,然后ngixn返回服务器信息。
可以在nginx的docker-compose的环境配置中写好ip和端口这样就可以不修改conf文件了。

docker配置

项目路径

docker_net_work_test/ # 项目根目录 ├── nginx/ # Nginx 服务目录 │ ├── html/ # WebGL 前端文件 │ │ ├── Build/ # Unity WebGL 构建输出 │ │ ├── StreamingAssets/ # Unity 流媒体资源 │ │ └── index.html # 入口 HTML 文件 │ ├── Dockerfile # Nginx 镜像构建文件 │ └── nginx.conf # Nginx 配置文件 ├── node/ # Node.js 后端服务目录 │ ├── node_modules/ # Node.js 依赖包 │ ├── .env # 环境变量配置 │ ├── app.js # Node.js 主应用文件 │ ├── Dockerfile # Node.js 镜像构建文件 │ ├── package.json # Node.js 项目配置 │ └── package-lock.json # 依赖锁文件 ├── docker-compose.nginx.yml # Nginx 服务编排配置 └── docker-compose.node.yml # Node.js 服务编排配置

项目运行

先运行node服务
docker-compose -f docker-compose.node.yml up -d
运行nginx服务
docker-compose -f docker-compose.nginx.yml up -d

前端页面:
http://主机IP:58231

nodejs服务
http://主机IP:13000/

nodejs服务配置

docker-compose.node.yml
services:node:build:./node# 基于node目录下的 Dockerfile 构建镜像container_name:node-app# 容器运行时的名字ports:-"13000:13000"# 宿主机:容器 端口映射networks:-backend# 定义属于的网络名称, backend 网络networks:backend:name:backend-net# 网络名称

app.js

/******************************************************************** * - HTTP 13000 (/ /api/health) * 依赖:npm i morgan cors dotenv *******************************************************************/require('dotenv').config();consthttp=require('http');constexpress=require('express');constcors=require('cors');constmorgan=require('morgan');/* ------------------ 配置 ------------------ */constHTTP_HOST=process.env.HTTP_HOST||'0.0.0.0';constHTTP_PORT=process.env.HTTP_PORT||13000;constNODE_ENV=process.env.NODE_ENV||'development';/* ------------------ Express ------------------ */constapp=express();// CORS 配置if(['localhost','development','dev'].includes(NODE_ENV)){app.use(cors());console.log('(本地调试)/(开发),使用跨域');}elseif(['production','pro'].includes(NODE_ENV)){console.log('生产环境不使用跨域');}else{app.use(cors());console.log('未知环境使用跨域');}// 日志中间件morgan.token('date',()=>newDate().toLocaleString('zh-CN'));app.use(morgan(':method :url code:status contentLength:res[content-length] - :response-time ms :date'));app.use(express.json());/* ------------------ 路由 ------------------ */app.get('/api/config',(req,res)=>{console.log('headers:',req.headers);// 1. 优先用代理头letport=req.get('X-Real-Port')||req.get('X-Forwarded-Port');// 2. 代理头都没有 → 从 Referer 提取if(!port){constreferer=req.get('Referer');constm=referer&&referer.match(/:(\d+)\//);port=m?m[1]:(req.secure?443:80);}constproto=req.get('X-Forwarded-Proto')||req.protocol;consthost=req.get('X-Forwarded-Host')||req.get('Host');consthostWithPort=host.includes(':')?host:`${host}:${port}`;constbaseUrl=`${proto}://${hostWithPort}`;res.json({success:true,baseUrl,host:hostWithPort,proto,environment:NODE_ENV});});// 请求方式测试使用apiapp.get('/api/health',(req,res)=>res.json({success:true,message:'服务健康',environment:NODE_ENV,timestamp:newDate().toISOString()}));app.get('/',(req,res)=>res.json({success:true,message:'HTTP 服务正常',environment:NODE_ENV,paths:{http:'/',config:'/api/config',health:'/api/health',post:'/post',put:'/put',delete:'/delete'}}));app.post('/post',(req,res)=>res.json({success:true,method:'POST',message:'POST 通道正常',timestamp:newDate().toISOString(),body:req.body}));app.put('/put',(req,res)=>res.json({success:true,method:'PUT',message:'PUT 通道正常',timestamp:newDate().toISOString(),body:req.body}));app.delete('/delete',(req,res)=>res.json({success:true,method:'DELETE',message:'DELETE 通道正常',timestamp:newDate().toISOString()}));app.use((req,res)=>res.status(404).json({success:false,message:'路由不存在',path:req.path}));/* ------------------ 创建 HTTP 服务器 ------------------ */consthttpServer=http.createServer(app);/* ------------------ 启动 HTTP ------------------ */httpServer.listen(HTTP_PORT,HTTP_HOST,()=>{console.log(`🚀 HTTP 服务 http://${HTTP_HOST}:${HTTP_PORT}`);console.log(`⚡ 环境${NODE_ENV}`);console.log(`📡 可用路由:`);console.log(`GET / - 服务状态`);console.log(`GET /api/config - 配置信息`);console.log(`GET /api/health - 健康检查`);console.log(`POST /post - POST 测试`);console.log(`PUT /put - PUT 测试`);console.log(`DELETE /delete - DELETE 测试`);});
Dockerfile
FROM node:22-alpine WORKDIR /app COPY app.js . RUN npm i express morgan cors dotenv EXPOSE 13000 CMD ["node","app.js"]

使用node:22-alpine镜像,拷贝node下的app.jsapp文件夹下,并安装包,暴露端口是13000,与docker-compose中的端口暴露的一致。并启动服务。

package.json
{ "name": "unity-webgl-server", "version": "1.0.0", "main": "app.js", "scripts": { "start": "node app.js", "dev": "nodemon app.js" }, "dependencies": { "aedes": "^0.51.3", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.18.2", "morgan": "^1.10.1", "websocket-stream": "^5.5.2", "ws": "^8.13.0" }, "devDependencies": { "nodemon": "^3.0.1" } }
.env
HTTP_HOST=0.0.0.0 HTTP_PORT=13000 # 应用配置(localhost/development/production) NODE_ENV=localhost

nginx配置

配置ngixn,并将html文件拷贝的到容器中,使用外部的配置文件,端口暴露58231对外,8080使用docker内部的端口,网络使用和node在同一个网络下。

docker-compose.nginx.yml
services:nginx:build:./nginx# 构建镜像container_name:nginx-webports:-"58231:8080"# 宿主机 58231 映射到容器 8080networks:-backend# 跟 node 同一网络networks:backend:name:backend-net#复用网络external:true# 不新建网络
Dockerfile
FROM nginxinc/nginx-unprivileged:1.29.0-alpine3.22 COPY nginx.conf /etc/nginx/nginx.conf COPY html/ /usr/share/nginx/html/ USER root

使用nginxinc/nginx-unprivileged:1.29.0-alpine3.22镜像,可以使用自己的镜像。
拷贝nginx.conf,拷贝html文件
使用root权限。

nginx.conf
worker_processes 1; events { worker_connections 1024; } http { # 引入MIME类型映射表 include mime.types; default_type application/octet-stream; # 开启零拷贝发送文件 sendfile on; keepalive_timeout 65; # 虚拟主机配置 server { # 监听端口 listen 8080; # 服务绑定地址:允许所有地址访问 server_name 0.0.0.0; # Docker容器内Node服务反向代理 location /docker_s/ { # 代理转发地址:指向Docker内的node-app服务(13000端口) proxy_pass http://node-app:13000/; # 传递客户端真实访问端口(优先取上层Nginx的X-Real-Port,无则用当前服务端口) proxy_set_header X-Forwarded-Port $http_x_real_port$server_port; proxy_set_header X-Real-Port $http_x_real_port$server_port; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 直接使用nginx提供API配置信息 location = /nginx_s/api/config { set $api_proto "http"; set $api_port "58231"; set $api_env "production"; set $api_host "$host:$api_port"; # 设置响应头为JSON格式 add_header Content-Type application/json; return 200 "{ \ \"success\":true, \ \"baseUrl\":\"$api_proto://$api_host\", \ \"host\":\"$api_host\", \ \"proto\":\"$api_proto\", \ \"environment\":\"$api_env\" \ }"; } # 前端静态资源入口 location / { # 静态资源根目录 root /usr/share/nginx/html; # 默认索引文件 index index.html index.htm; # SPA路由兼容:匹配不到文件时重定向到index.html try_files $uri $uri/ /index.html; } # # 5xx服务器错误页面配置 # error_page 500 502 503 504 /50x.html; # location = /50x.html { # root html; # } } }

location /docker_s/ {返回将请求反向代理到容器内部的服务器。这里要使用docker的容器名称, 虽然在一个网络里面,使用localhost会访问nginx的本机,而不是访问的实际的服务。

location = /nginx_s/api/config {在nginx中直接返回,因为有时候会获取不到服务的ip地址。

unity代码

unity 使用tmp,使用unitybesthttpv3 进行网络通信。
besthttpv3 不支持路由请求,所以使用unitywebrequest请求获取服务地址,然后在使用unitybesthttpv3进行请求。

usingUnityEngine;usingUnityEngine.UI;usingUnityEngine.Networking;usingSystem.Collections;usingBest.HTTP;usingTMPro;namespaceAssets{internalclasshttp_test_unityrequest:MonoBehaviour{[SerializeField]publicTMP_InputFieldurlInputField_unity;[SerializeField]publicTMP_InputFieldurlInputField_besthttp;[SerializeField]publicButtonunityWebRequestButton;[SerializeField]publicButtonbestHttpRequestButton;[SerializeField]publicTMP_TextresponseText;privatevoidStart(){unityWebRequestButton.onClick.AddListener(()=>StartCoroutine(SendUnityWebRequest()));bestHttpRequestButton.onClick.AddListener(()=>StartCoroutine(SendBestHTTPRequest()));}privateIEnumeratorSendUnityWebRequest(){responseText.text=$"UnityWebRequest: 请求中...";using(UnityWebRequestrequest=UnityWebRequest.Get(urlInputField_unity.text)){yieldreturnrequest.SendWebRequest();responseText.text=request.result!=UnityWebRequest.Result.Success?$"UnityWebRequest错误:\n{request.error}\n{request.downloadHandler?.text}":$"UnityWebRequest成功:\n{request.downloadHandler?.text}";}}privateIEnumeratorSendBestHTTPRequest(){responseText.text=$"BestHTTP: 请求中...";boolrequestCompleted=false;stringresult="";newHTTPRequest(newSystem.Uri(urlInputField_besthttp.text),(req,res)=>{result=res==null?"请求失败:响应为空":!res.IsSuccess?$"BestHTTP错误:\n{res.Message}\n{res.DataAsText}":$"BestHTTP成功:\n{res.DataAsText}";requestCompleted=true;}).Send();while(!requestCompleted)yieldreturnnull;responseText.text=result;}}}

测试html代码

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>GET请求发送器</title> </head> <body> <h1>GET请求发送器</h1> <div> <label for="urlInput">请输入请求路径:</label> <br> <input type="text" id="urlInput" placeholder="/api/" size="40" value="/api/"> </div> <br> <button onclick="sendGetRequest()">发送GET请求</button> <br><br> <div id="responseArea"> <h3>响应结果:</h3> <pre id="responseText">等待请求...</pre> </div> <script> function sendGetRequest() { const urlInput = document.getElementById('urlInput'); const responseText = document.getElementById('responseText'); const path = urlInput.value.trim(); if (!path) { responseText.textContent = '错误: 请输入有效的路径'; return; } console.log('请求路径:', path); responseText.textContent = `正在请求: ${path}`; // 直接获取响应,不做任何状态判断 fetch(path) .then(response => { console.log('响应状态:', response.status, response.statusText); console.log('响应URL:', response.url); // 无论什么状态码,都返回响应文本 return response.text().then(text => { return { status: response.status, statusText: response.statusText, headers: Object.fromEntries([...response.headers.entries()]), url: response.url, body: text }; }); }) .then(result => { // 显示完整的响应信息 responseText.textContent = `响应状态: ${result.status} ${result.statusText} 响应URL: ${result.url} 响应头: ${JSON.stringify(result.headers, null, 2)} 响应体: ${result.body}`; }) .catch(error => { responseText.textContent = `请求异常: ${error.message}`; }); } document.addEventListener('DOMContentLoaded', function() { document.getElementById('urlInput').value = '/api/'; }); </script> </body> </html>

项目测试

运行项目后访问http://localhost:58231/然后发送请求进行测试,可以转到不同的location块。

转发docker_s/api/health

使用besthttpV3进行请求

网络命令

网络与容器关系命令

$net='backend-net'; write-host "网络名称: $net"; docker network inspect $net -f '{{range .Containers}}{{.Name}}{{println}}{{end}}' | Where-Object { $_ -ne '' } | ForEach-Object { $cid = docker ps -q --no-trunc --filter name=$_; if ($cid) { write-host "容器名称: $_ 容器ID: $($cid.Substring(0,12))" } }

可以查看网络下有那些容器。
输出

PS C:\Users\GoodCooking> $net='backend-net'; write-host "网络名称: $net"; docker network inspect $net -f '{{ra nge .Containers}}{{.Name}}{{println}}{{end}}' | Where-Object { $_ -ne '' } | ForEach-Object { $cid = docker ps -q --no-trunc --filter name=$_; if ($cid) { write-host "容器名称: $_ 容器ID: $($cid.Substring(0,12))" } } 网络名称: backend-net 容器名称: node-app 容器ID: 53a5b8a00a08 容器名称: nginx-web 容器ID: e7ad62d8745b PS C:\Users\GoodCooking>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/22 23:28:24

飞书文档导出终极解决方案:告别手动下载的烦恼

飞书文档导出终极解决方案&#xff1a;告别手动下载的烦恼 【免费下载链接】feishu-doc-export 项目地址: https://gitcode.com/gh_mirrors/fe/feishu-doc-export 还在为飞书文档的本地备份而头疼吗&#xff1f;曾经我也面临着同样的困扰&#xff0c;直到发现了feishu-…

作者头像 李华
网站建设 2026/4/18 8:12:12

深蓝词库转换:免费开源输入法词库格式互转终极指南

深蓝词库转换&#xff1a;免费开源输入法词库格式互转终极指南 【免费下载链接】imewlconverter ”深蓝词库转换“ 一款开源免费的输入法词库转换程序 项目地址: https://gitcode.com/gh_mirrors/im/imewlconverter 在数字化时代&#xff0c;不同输入法之间的词库格式不…

作者头像 李华
网站建设 2026/4/21 17:50:01

5大常见OCR识别难题,Umi-OCR如何帮你轻松解决?

还在为图片中的文字识别而烦恼吗&#xff1f;无论是截图中的代码片段、PDF文档的批量处理&#xff0c;还是多语言文本的准确提取&#xff0c;Umi-OCR作为一款免费开源的离线OCR软件&#xff0c;都能为你提供专业级的解决方案。本文将带你了解如何通过Umi-OCR应对各种OCR识别挑战…

作者头像 李华
网站建设 2026/4/18 16:22:56

MsgViewer:跨平台邮件文件处理利器

在当今数字化办公环境中&#xff0c;邮件已成为不可或缺的沟通工具。然而&#xff0c;当遇到特殊的邮件文件格式时&#xff0c;很多用户会感到束手无策。MsgViewer作为一款纯Java实现的邮件查看工具&#xff0c;完美解决了这一痛点&#xff0c;让邮件查看变得简单高效。 【免费…

作者头像 李华
网站建设 2026/4/19 4:22:14

终极密钥配置指南:轻松解锁Zotero-GPT全部潜能

还在为Zotero-GPT插件报错"your secretKEY is not configured"而烦恼吗&#xff1f;这个看似复杂的技术问题&#xff0c;其实就像你新买的手机需要插入SIM卡才能打电话一样简单。API密钥就是Zotero-GPT连接智能服务的"SIM卡"&#xff0c;没有它&#xff0c…

作者头像 李华
网站建设 2026/4/21 12:13:13

3步搞定Steam成就管理:告别繁琐操作的游戏神器

还在为Steam成就解锁而烦恼吗&#xff1f;每次想查看成就进度都要退出游戏&#xff0c;切换窗口&#xff0c;这种体验简直让人抓狂&#xff01;今天&#xff0c;就让我带你用SteamAchievementManager这款神器&#xff0c;彻底告别成就管理的痛苦历程。 【免费下载链接】SteamAc…

作者头像 李华