Stories

Detail Return Return

你用過docker部署前端項目嗎?Tell Me Why 為何要用docker部署前端項目呢? - Stories Detail

需求開發場景

在説docker之前,我們先來看看一般的需求開發和部署場景,是否需要安裝node

需求開發部署場景

開發環境,我們使用windows或mac,開發前端項目,正常來説,都是要安裝好對應node版本 ,使用node提供的npm包管理(構建)工具【除非是一個簡單的只有hello world的html文件不用node】

生產環境,要發佈到服務器上

  • 1. 靜態SPA單頁面應用部署
  • 服務器上不需要安裝nodejs,使用使用nginx代理一下請求即可
  • 就算是多個單頁面項目,我們在本地開發使用nvm管理一下node版本(比如有node12的老項目,也有node24的新項目)
  • 打包的dist,丟到服務器上後,依舊不需要在在服務器上安裝node

  • 2. SSR服務端渲染部署
  • 除了靜態SPA以外,我們也可能也要去寫SSR應用
  • SSR實際上就是通過nodejs運行環境,在服務器上執行js代碼
  • 比如解析路由、發請求拿後端數據、拼接生成html返回給前端的瀏覽器請求
  • 因此,SSR服務端渲染的生產環境的部署,服務器上,必須安裝nodejs(當然,也要使用到nginx代理請求)
  • 如果,某個服務器上,只是部署一個SSR項目還好,我們只需要安裝對應的node版本
  • 但是,如果有兩個甚至多個SSR項目,且對應的node版本不一致(如需要node12和node24版本)
  • 那麼,我們就需要在服務器上,安裝nvm進行node版本的管理,不同的node版本前端項目,使用nvm切換到對應的node版本,然後,npm start跑起來項目【可使用pm2管理多進程】

  • 3. BFF中間層部署
  • BFF中間層,相當於一個服務層(中間層)
  • 就是,把後端的 “通用接口” 轉化為前端的 “專屬接口”
  • 比如,使用Express/Koa啓一個服務(依賴node)(當然,也要使用到nginx代理請求)
  • 流程:用户 → 前端應用(PC/移動端/小程序) → BFF 中間層 → 後端服務 → 去數據庫撈數據
  • 同樣的,這個情況,和上述一樣
  • 安裝nvm進行node版本的管理,不同的BFF中間層,要切換對應node才能跑起來【可使用pm2管理多進程】

Web前端常見的三種部署方式

  1. 對於Vue或React的單頁面應用,打包的dist靜態資源,再搭配nginx
  2. 對於ssr(服務器渲染)或者bff(接口中間層),使用nvm管理node版本,同時使用pm2統一管理,再搭配nginx
  3. 使用docker鏡像技術,一次構建,到處運行(最靈活的方式),基本上適合所有的前端部署方式。(輔以nginx)

接下來,説説docker鏡像部署的好處

docker鏡像部署前端項目的好處

1. 徹底解決環境一致性,不用再使用nvm做node版本切換

初學者,可以把docker容器鏡像理解成:一個依賴宿主機的、封閉的、‘微型’服務器的內存運行環境空間嗎(非虛擬機那麼冗餘、且憑藉宿主機的操作系統內核可在此內存運行環境空間跑服務程序)

  • 假設多個ssr或者bff且node版本依賴不同的項目,有幾個,就打包幾個鏡像
  • 可以對應依賴的node版本等打包到鏡像裏面(當然nginx也可以選擇連帶着打包到鏡像裏面)
  • 鏡像與鏡像之間是獨立的,雖然node依賴版本不一樣,但是ssr的服務不會和bff的服務產生衝突
  • 不用再想以前那樣,還得額外注意node版本的切換管理
  • 收益,比較明顯!!!

2. 實現 “一次構建,各個服務器環境上都能直接運行”

無論是Linux還是Windows服務器,都能運行打包好的鏡像(可移植性強)

  • 假設,有一天,原來的生產服務器爆炸了、死機了、因不可抗力直接掛了。
  • 老闆趕緊買了一台新的服務器,讓把原來生產的前端項目,移植到新的服務器上,越快越好。
  • 傳統情況就是,在新的服務器上裝各個版本的node,安裝nvm,再打包使用pm2管理部署(耗時,約為一個小時)
  • 但是,如果是使用docker,直接把原本的鏡像,複製粘貼到新服務器上即可(耗時約10分鐘)
  • 收益,十分明顯!!!

3. docker版本管理與回滾更簡單方便安全

  • 假設新項目上線後發現bug,需緊急切回上一版本
  • 若是傳統項目部署方式,需要本地git回滾,再重新打包,再發布到服務器上(耗時5分鐘)
  • 若是docker鏡像部署方式,直接使用其自帶的版本標籤tag管理
  • 每個版本的項目對應一個鏡像標籤(如 v1.0.0v1.0.1),標籤與代碼版本一一對應,可追溯回滾
  • 回滾時,只需停止當前容器,用舊版本標籤的鏡像重新啓動容器(如 docker run my-app:v1.0.0
  • 整個過程秒級完成,且不會影響當前文件(容器銷燬後文件自動清理),安全可靠(耗時1分鐘)
  • 收益,十分明顯!!!

4. docker部署流程標準化、自動化

  • 傳統前端部署流程通常是
  • 本地打包 → 用winscp等工具傳文件到服務器 → 然後手動配置ngixn或者手動啓動node服務
  • 步驟繁瑣且依賴人工操作,有一定概率手抖了,人工操作出問題(雖然概率不大,但也是一個隱患,假設概率千分之一)
  • 如果使用docker搭配cicd持續集成工具可以將部署流程自動化
  • 實現 代碼在gitlab提交後 → 點一點,就能夠自動構建鏡像 → 然後自動推送到鏡像倉庫 → 最後服務器自動拉取鏡像並重啓容器
  • 全程無需人工干預,基本不會出問題(假設出問題概率百萬分之一)
  • 節省人工操作時間、隱患概率大大降低
  • 收益,十分明顯!!!

如果只是簡單的spa單頁面應用的部署,且暫時沒有cicd工具的公司項目,也可以自己搞一個效能工具,類似於cicd的自動化發佈腳本,參考筆者的這篇文章:https://segmentfault.com/a/1190000044616092

實際上,因為前端部署項目的依賴比較少,主要是可能依賴node環境,如果是後端部署項目,那依賴的就多了,使用docker技術的優勢,能夠進一步加大的明顯體現出來

記錄docker部署一個簡單的單頁面spa前端項目

提前啓動電腦的虛擬化、並下載安裝 Docker Desktop、並啓用 WSL 2

首先,windows電腦的虛擬化要開啓的,如下圖

按下Ctrl + Shift + Esc 打開任務管理器、然後選擇 性能 標籤

然後,打開 powershell 執行 wsl --list --online,查看可安裝的linux發行版,初始條件下,肯定是沒安裝的,然後執行 wsl --install 自動安裝默認linux發行版(筆者用的是Ubuntu)

再然後,就是安裝完成後重啓電腦,使 WSL2 生效

最後,訪問 Docker 官網 下載適用於 Windows或者MAC 的安裝包,這樣docker的前置準備工作,就做好了

本文不做安裝的贅述,可以另行查閲文章,實踐安裝(若是網絡不行,就使用小梯子吧)

單頁面應用的docker打包

  • 假設,筆者有一個vue或者react項目
  • 這個項目最終,打包成了一個html文件,如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>每天學點新知識——docker</h1>
</body>
</html>

編寫Dockerfile文件

  • 想要使用docker打包鏡像,就得告訴docker,這個鏡像要打包那些東西
  • 本案例中,是打包一個單純的html文件
  • 同時,還要告訴docker,有哪些依賴也需要連帶着打包進去
  • 本案例中,打包單純的html文件不太夠用
  • 還需要搭配nginx(把nginx也打包進鏡像中去)
  • 沒辦法,單純的靜態文件,沒法自己提供網頁服務,必須搭配一個 “服務器軟件”
  • 這樣打包出來的鏡像,就像一個 “自帶服務器的小盒子”,不管放到哪台裝了 Docker 的服務器上的機器上,都能直接跑起來

本案例的Dockerfile文件的編寫很簡單,就四行

  • FROM nginx:alpine
  • COPY index.html /usr/share/nginx/html/
  • EXPOSE 80
  • CMD ["nginx", "-g", "daemon off;"]

註釋,釋義如下

# 默認從 Docker Hub上下載基於Alpine Linux的輕量級版本的nginx,當執行docker打包鏡像命令後,流程是:
# 自己windows電腦的命令行會觸發Docker Desktop依據 WSL2 Linux內核從而下載nginx:alpine到WSL2文件系統
# 自己的nginx:alpine會下載到C:\Users\lss13\AppData\Local\Docker\wsl\disk文件夾中
# 有一個docker_data.vhdx硬盤映像文件
# 文件很大,類似壓縮包,包含很多東西,其中就有下載的nginx:alpine鏡像,也有構建出的新鏡像和以往構建的老鏡像
FROM nginx:alpine

# 將當前目錄下的HTML文件複製到鏡像中的/usr/share/nginx/html/目錄
# 鏡像最終存儲在docker_data.vhdx虛擬磁盤中
# /usr/share/nginx/html/這個文件夾路徑,是nginx用來默認存放靜態資源的路徑(規定,不用去修改)
# 至此,鏡像文件中,已經包含了nginx的一堆東西和html,當然還有別的docker的一堆東西
COPY index.html /usr/share/nginx/html/

# EXPOSE不會實際開放端口,單純的語法,不寫也行(NGINX默認就是80端口)
EXPOSE 80

# 啓動nginx  -g是全局配置命令  daemon off關閉後台運行模式
# (能夠確保 nginx 前台運行,避免容器啓動後立即退出)
# 這個cmd指令,會被存放在鏡像文件中,當鏡像被丟到服務器上後
# 當在服務器上執行docker run這個鏡像的時候,才會進一步觸發鏡像裏面的這個cmd命令執行
# 才會讓鏡像中的nginx啓動起來,有這樣的web服務,才能訪問到鏡像裏面的html文件
CMD ["nginx", "-g", "daemon off;"]

編寫打包構建鏡像的js腳本

構建鏡像,就一個核心的命令:docker build -t ${IMAGE_NAME} .

  • docker build:Docker 的構建命令,告訴 Docker “我要根據 Dockerfile文件裏面的內容去構建鏡像了”。
  • -t ${IMAGE_NAME}:給鏡像 “貼標籤”(指定名稱),比如 -t my-nginx 就會把鏡像命名為 my-nginx${IMAGE_NAME} 通常是一個變量,實際使用時會替換成具體名稱,比如 my-webapp:v1)。
  • .:指定 Dockerfile 所在的路徑(. 表示 “當前目錄”),Docker 會從這個目錄找 Dockerfile 文件,並讀取裏面的構建步驟。
當然,現在的我的 html 和 Dockerfile 和 要準備編寫的 打包構建鏡像的js腳本 在同一個文件夾裏面(同級目錄)

主要的核心,就是使用node的child\_process的execSync,在命令行執行docker命令:

const { execSync } = require('child_process'); // 引入同步執行命令模塊

const IMAGE_NAME = 'my-html-app'; // 給鏡像起個名字

execSync(`docker build -t ${IMAGE_NAME} .`, { stdio: 'inherit' }); // 派發命令執行打包構建鏡像

完整export-image.js腳本如下:

const { execSync } = require('child_process'); // 引入同步執行命令模塊
const fs = require('fs'); // 引入文件系統模塊

console.log('📦 開始構建和導出Docker鏡像...');

// 配置變量
const IMAGE_NAME = 'my-html-app';
const TAR_FILE = `${IMAGE_NAME}.tar`;

try {
    // 檢查Dockerfile是否存在
    if (!fs.existsSync('./Dockerfile')) {
        console.error('❌ 找不到Dockerfile文件');
        process.exit(1);
    }

    // 檢查index.html是否存在
    if (!fs.existsSync('./index.html')) {
        console.error('❌ 找不到index.html文件');
        process.exit(1);
    }

    console.log('🔨 正在構建Docker鏡像...');
    
    // 構建Docker鏡像
    execSync(`docker build -t ${IMAGE_NAME} .`, { stdio: 'inherit' });
    
    console.log('\n✅ 鏡像構建成功,開始導出鏡像...');
    
    // 刪除舊的tar文件(如果存在)
    if (fs.existsSync(TAR_FILE)) {
        fs.unlinkSync(TAR_FILE);
        console.log('🗑️  已刪除舊的鏡像文件');
    }
    
    // 導出鏡像
    execSync(`docker save -o ${TAR_FILE} ${IMAGE_NAME}:latest`, { stdio: 'inherit' });
    
    // 檢查文件是否成功創建
    if (fs.existsSync(TAR_FILE)) {
        const stats = fs.statSync(TAR_FILE);
        const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
        console.log('\n✅ 鏡像導出成功!');
        console.log(`📁 導出文件: ${TAR_FILE} 文件大小: ${fileSizeMB} MB`);
        console.log('\n📋 接下來:');
        console.log('1. 複製 my-html-app.tar 和 deploy-to-server.sh 到Ubuntu');
        console.log('2. 在Ubuntu上運行: ./deploy-to-server.sh');
        
    } else {
        console.error('❌ 鏡像導出失敗');
        process.exit(1);
    }
    
} catch (error) {
    console.error('\n❌ 操作失敗:');
    console.error(error.message);
    process.exit(1);
}
  • 我們可以在命令行中,執行這個腳本,比如:node export-image.js
  • 也可以,寫一個bat腳本,這樣也行,直接./build.bat回車

build.bat

@echo off
echo Let's start building Docker images...
node export-image.js

執行構建腳本

構建出來的鏡像產物

編寫服務器發佈鏡像的shell腳本

邏輯很簡單,就是把當前服務器上的,原來的容器鏡像刪除掉(如果有的話),然後,在執行docker run -d -p $PORT:80 --name $CONTAINER_NAME $IMAGE_NAME:latest 命令

上述命令釋義:

  • docker run(Docker 啓動容器的核心命令)
  • -d (後台運行模式detach 的縮寫)
  • -p $PORT:80

    • -p是端口映射publish的縮寫
    • $PORT 是筆者自己的服務器(宿主機)端口,我這裏用20000端口,外部通過這個端口訪問容器;
    • 80 是容器內部的端口(Nginx 默認監聽 80 端口)
    • 意思是:把我服務器(宿主機)上的20000端口,和容器的80端口連起來,外部訪問我服務器上的20000端口,就會被轉到這個docker容器鏡像的80端口,也就會訪問到鏡像裏面的nginx,也就能夠訪問到鏡像裏面的對應目錄/usr/share/nginx/html/中的index.html文件(就能看到對應內容了)

當然了 服務器不隨便開放端口,這個20000端口,我不會開放,我只會使用我服務器上nginx,進行請求的轉發到20000端口,如下 nginx 配置

# docker的demo
location /dockerDemo/ {
    proxy_pass http://localhost:20000/;
    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;
}

部署腳本如下:

#!/bin/bash

echo "🚀 開始部署Docker鏡像到服務器..."

# 配置變量(鏡像名稱在構建時指定,容器名稱在運行時指定)
CONTAINER_NAME="html-app-container"
IMAGE_NAME="my-html-app"
TAR_FILE="${IMAGE_NAME}.tar"
PORT="20000"

# 檢查tar文件是否存在
if [ ! -f "$TAR_FILE" ]; then
    echo "❌ 找不到鏡像文件: $TAR_FILE"
    echo "請確保已將鏡像文件複製到當前目錄"
    exit 1
fi

echo "📁 找到鏡像文件: $TAR_FILE"

# 停止並刪除現有容器(如果存在)
echo "🛑 停止並刪除現有容器..."
docker stop $CONTAINER_NAME 2>/dev/null || true
docker rm $CONTAINER_NAME 2>/dev/null || true

# 刪除現有鏡像(如果存在)
echo "🗑️  刪除現有鏡像..."
docker rmi $IMAGE_NAME:latest 2>/dev/null || true

# 導入鏡像
echo "📥 導入Docker鏡像..."
docker load -i $TAR_FILE

# 運行容器
echo "🚀 啓動新容器..."
docker run -d -p $PORT:80 --name $CONTAINER_NAME $IMAGE_NAME:latest

# 檢查容器狀態
if [ $? -eq 0 ]; then
    echo ""
    echo "✅ 部署成功!"
    echo "📊 容器狀態:"
    docker ps | grep $CONTAINER_NAME
    echo ""
    echo "🌐 訪問地址:"
    echo "   直接訪問: http://localhost:$PORT"
    echo "   通過nginx代理: https://ashuai.site/dockerDemo/"
    echo ""
    echo "📋 有用的命令:"
    echo "   查看日誌: docker logs $CONTAINER_NAME"
    echo "   停止容器: docker stop $CONTAINER_NAME"
    echo "   重啓容器: docker restart $CONTAINER_NAME"
else
    echo "❌ 容器啓動失敗"
    exit 1
fi

最後一步,在服務器上,部署構建好的鏡像

把鏡像文件和構建腳本都丟到服務器上,在對應文件夾中,執行部署腳本,如下圖:

  • 這樣,我們的構建好了的,鏡像,就成功部署了
  • 由於筆者是通過nginx代理的
  • 所以,訪問:https://ashuai.site/dockerDemo/
  • 就可以 看到對應的內容了

docker-compose的應用場景——統一編排管理多個鏡像服務

  • 比如我現在有一個項目,其中包括了,前端、後端、數據庫、緩存層
  • 對應的我要在本地電腦上,打包構建四個鏡像,分別是nginx.tar、mysql.tar、java-app.tar、redis.tar
  • 然後把這四個鏡像上傳到服務器上(通過dockerhub也行)
  • 針對以這樣的部署應用場景 如果不使用docker-compose,就得手動執行如下的幾次docker腳本命令,如下:
# 1. 創建自定義網絡,讓容器可以互相通信
docker network create app-network

# 2. 啓動 MySQL
docker run -d \
  --name mysql-container \
  --network app-network \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -v mysql-data:/var/lib/mysql \
  mysql:latest

# 3. 啓動 Redis
docker run -d \
  --name redis-container \
  --network app-network \
  -p 6379:6379 \
  redis:alpine

# 4. 啓動 Java 應用(依賴 MySQL 和 Redis)
docker run -d \
  --name java-app-container \
  --network app-network \
  -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-container:3306/mydb \
  java-app:latest

# 5. 啓動 Nginx(前端)
docker run -d \
  --name nginx-container \
  --network app-network \
  -p 80:80 \
  nginx:latest

當然 ,也可以把上述的五次命令,都放在一個shell腳本里面,但是不優雅

使用docker-compose.yml優雅地管理同一個項目中的多個鏡像

  • 首先,docker-compose.yml 默認就會讓所有服務(比如我的四個鏡像)處於同一個網絡中,實現自動互通,自帶此功能
  • 其次,有了docker-compose.yml以後,就可以通過docker-compose up -d一鍵啓動所有服務,也可以通過docker-compose down一鍵關閉所有服務
  • 然後,將整個應用棧(前端、後端、數據庫、緩存)的配置都被記錄在一個 .yaml 文件中。這樣方便,做版本控制,追蹤每一次變更;保持環境一致性、易於團隊分享和協作
  • 可以在docker-compose.yml文件中,設置鏡像服務啓動順序(比如,要先啓動mysql和redis,然後再啓動java-app,最後再啓動nginx——有服務依賴先後順序
  • 另外,日誌,也能夠統一管理,等等等等的好處
  • docker-compose的功能強大,管理優雅,贊👍👍👍

簡單如下示例,實際項目,會稍微完整完善不少

# 定義 Compose 文件版本
version: '3.8'

services:
  # 後端 Java 應用服務
  java-app:
    image: java-app:latest # 使用本地構建的 Java 應用鏡像
    container_name: my-java-app # 指定容器名稱
    environment:
      # 配置數據庫連接地址(通過服務名訪問)
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/my_database
      # 配置 Redis 連接地址
      - SPRING_REDIS_HOST=redis
    depends_on:
      - mysql # 依賴 MySQL 服務先啓動
      - redis # 依賴 Redis 服務先啓動
    networks:
      - app-network # 加入應用內部網絡

  # 前端 Nginx 服務
  nginx:
    image: nginx:latest # 使用 Nginx 鏡像
    container_name: my-nginx # 指定容器名稱
    ports:
      - "80:80" # 將主機80端口映射到容器80端口
    volumes:
      # 掛載自定義 Nginx 配置文件
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - java-app # 依賴 Java 應用先啓動
    networks:
      - app-network # 加入應用內部網絡

  # MySQL 數據庫服務
  mysql:
    image: mysql:8.0 # 使用 MySQL 8.0 鏡像
    container_name: my-mysql # 指定容器名稱
    environment:
      # 設置 MySQL root 用户密碼
      MYSQL_ROOT_PASSWORD: rootpassword
    volumes:
      # 持久化數據庫數據到主機
      - mysql_data:/var/lib/mysql
    networks:
      - app-network # 加入應用內部網絡

  # Redis 緩存服務
  redis:
    image: redis:7-alpine # 使用 Redis Alpine 版本(輕量)
    container_name: my-redis # 指定容器名稱
    networks:
      - app-network # 加入應用內部網絡

# 定義自定義網絡,使服務間可通過服務名通信
networks:
  app-network:
    driver: bridge # 使用橋接網絡驅動

# 定義數據卷,用於持久化數據
volumes:
  mysql_data: # MySQL 數據持久化卷

此外,Docker Compose還可以做服務健康檢查和服務等待機制,能通過各種策略,能夠讓部署更可靠,具體看文檔:https://docs.docker.com/compose/

其他:上述的depends\_on僅確保容器按順序啓動,但不確保服務就緒。需要配合應用的重試機制或Docker Compose的healthcheck和condition選項、Compose v2中支持

總結

docker鏡像打包,好像看起來麻煩一點點,還得寫Dockerfile,還得寫構建腳本,和部署腳本啥的,但是它解決了環境版本一致性問題,bff和ssr不同node版本,畢竟管理起來,還是有些麻煩的,還有服務器更換,要是重新安裝各種版本,那的確耗時。(打包前端項目,有點明顯、打包後端項目十分明顯)

如果打包bff,我們可以編寫如下的Dockerfile

# 基礎鏡像:用輕量的 Node.js 16 版本(alpine 版本體積小)
FROM node:16-alpine

# 創建工作目錄(類似在服務器上建一個專門的文件夾放代碼)
WORKDIR /app

# 複製 package.json 和 package-lock.json(先複製依賴文件,利用 Docker 緩存加速構建)
COPY package*.json ./

# 安裝依賴(npm install 會根據 package.json 下載所需的庫)
RUN npm install --production  # --production 只裝生產環境依賴,減小體積

# 複製 BFF 源代碼(比如 server.js、路由文件等)
COPY . .

# 暴露 BFF 服務的端口(假設我的BFF的服務監聽3000端口)
EXPOSE 3000

# 啓動命令:運行 BFF 服務
CMD ["node", "server.js"]

所以,docker的優勢有:

  1. 環境版本依賴一致性、隔離性與安全性
  2. 簡化團隊配置協作
  3. 可快速部署與擴展
  4. 而且,有了docker以後,CI/CD就更加好操作了(更好實現,標準化和自動化)
  5. 首次一勞————而後永逸
A good memory is better than a bad pen. Record it down...
user avatar grewer Avatar atguigu Avatar lenglengdechaomian Avatar huanjinliu Avatar chongdianqishi Avatar monkeynik Avatar huaweiclouddeveloper Avatar _58d8892485f34 Avatar jsliang Avatar yuxuan_6598fe8637146 Avatar magnesium_5a22722d4b625 Avatar hai2007 Avatar
Favorites 15 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.