博客 / 詳情

返回

2025 年前端開發工程師必備的 Docker Compose 全棧項目實踐

本文所涉及的代碼都在 jrainlau/todo-mvc-x-docker-compose,歡迎 star 歡迎打 call!

在當前的工作的項目中,我們大量使用了 Docker Compose 的相關技術。由於此前的工作和學習都缺乏相關的項目經驗,因此 Docker 的知識一直是我的短板,基本只停留在“知道是怎麼一回事,但沒有深入使用過”的淺層理解。面對項目中各種繁雜的 Docker 配置,一時半會之間差點應付不過來。為了補上這塊短板,我參考了項目的編排,DIY 了一個麻雀雖小卻五臟俱全的 mini 項目,旨在完整地體驗一次 Docker Compose 的玩法。

一、技術選型

如果説學習一門編程語言的第一步是“Hello World”,那麼學習一套全棧開發的第一步,一定非“TodoMVC”莫屬。我將構建一個前後端分離的 TodoMVC 應用,包含了呈現功能的前端頁面、提供接口的後端服務、存放數據的數據庫,實現完整的增刪改查功能。

image.png

時間來到了 2025 年,因此我將嘗試結合當前比較新穎的工具,來完成項目的搭建工作。

  • 前端
    採用 Vite + React + TS 來初始化和運行,使用 Bun 作為包管理工具。
  • 後端
    採用 Bun + Elysia + TS 來初始化和運行。
  • 數據庫
    MongoDB,因為足夠簡單,本地搭配 MongoDB Compass 可以非常直觀地查看數據庫內容。
  • 路由分發
    Nginx。配置簡單,通過 Nginx 對路由進行分發,實現同時對前後端模塊進行開發和聯調的目的。

值得注意的是,我在項目裏均使用了 Bun 這一工具來代替 NodeJS。它是一個集 JavaScript 運行時、打包工具、轉譯器於一體的工具,它具有高性能的特點,能在提升運行速度、優化打包過程等方面為 JavaScript 開發提供高效的解決方案。最重要的是它天生就直接支持 ESM 模塊和允許直接運行 TS,是非常值得嘗試的東西。

接下來,我們一步一步開始這個 TodoMVC 的應用搭建。

二、數據庫通過 Docker Compose 啓動

在項目開始之前,我們先要準備一個 MongoDB 數據庫。傳統的安裝運行方式已經過時,我們將直接通過 Docker 來啓動。在此之前,請確保你的電腦裏已經安裝了 Docker 運行時。這裏推薦使用 OrbStack,因為它足夠輕便、高效。由於 Docker hub 被封,沒有魔法的用户可以在 OrbStack 中配置騰訊的 Docker 鏡像源 https://mirror.ccs.tencentyun.com

image.png

來到項目根目錄,我們首先來新建一個 docker-compose.yml 文件,內容如下:

services:
  my-mongo:
    image: mongo
    ports:
      - "27017:27017"
    volumes:
      - ./.mongodb:/data/db #數據持久化,避免重啓容器後數據丟失

接下來通過終端運行 docker-compose up,即可自動拉取官方的 MongoDB 鏡像,啓動後把容器內的 27017 端口映射到本機的 27017 端口。我們可以通過 MongoDB Compass 這個工具來查看是數據庫已經能夠正常連接了:

image.png

當然,你也可以直接通過 Docker 指令直接把 MongoDB 跑起來

docker pull mongo

docker run --name my-mongo -p 27017:27017 mongo

但是作為有代碼潔癖的我來説,這種寫法冗長繁雜難以記憶又容易出錯,確實不如把參數變成配置來得好,而這也正是 Docker Compose 的優勢之一。

三、後端服務

對於前後端分離的全棧應用,我會優先完成提供接口的後端服務。

類似 Koa 之於 NodeJS,Elysia 是一個在 Bun 上運行,用於構建高性能、類型安全的 Web 應用程序的快速 HTTP 框架,基於 TypeScript,提供簡潔的 API 和高效的中間件系統來方便開發者創建服務器應用。

在項目的根目錄下,通過 Bun 來初始化一個基於 Elysia 的應用:

bun create elysia backend

項目初始化後,便可以在 backend/src/index.ts 中看到如下代碼:

image.png

接下來我們將對這部分代碼進行改造,完成一個最簡單的後端應用。它將分成 3 個部分進行:

  1. 連接 MongoDB 數據庫;
  2. 提供Restful 接口實現增刪查功能;
  3. 運行在 8080 端口

由於 Bun 能夠兼容 NodeJS 的絕大部分功能,因此我們也將採用在 NodeJS 裏熟悉的 mongoose 工具來操作數據庫。

bun install mongose

首先來定義一個 Todo 對象的 Schema:

const Todo = mongoose.model('Todo', new mongoose.Schema({
  text: String,
  completed: Boolean,
  updateTime: Date,
}));

接下來連接數據庫:

mongoose.connect('mongodb://my-mongo:27017/local')

注意,由於我們的項目將通過 Docker Compose 來運行,因此容器之間將通過“容器名稱”來相互調用。在上一節中我們啓動名為 my-mongo 的數據庫,因此這裏的連接 URL 也相應地寫成 my-mongo。

連接了數據庫以後,便可提供增刪改查的接口了。最後再監聽 8080 端口就大功告成。

mongoose.connect('mongodb://my-mongo:27017/local')
  .then(() => {
    console.log('Connected to MongoDB.');

    // Define the routes
    const app = new Elysia()
      .get('/api', () => 'Hello Elysia')
      .get('/api/todos', async () => {
        const todos = await Todo.find();
        return todos;
      })
      .post('/api/todos', async (req: any) => {
        const newTodo = new Todo({
          text: req.body.text,
          completed: false,
          updateTime: new Date(),
        });
        await newTodo.save();
        return newTodo.toJSON();
      })
      .patch('/api/todos', async (req: any) => {
        const { ids, completed } = req.body;
        await Todo.updateMany({ _id: { $in: ids } }, { completed });
        return { message: 'Todos updated' };
      })
      .delete('/api/todos', async (req: any) => {
        const { ids } = req.body;
        await Todo.deleteMany({ _id: { $in: ids } });
        return { message: 'Todos deleted' };
      })
      .listen(8080);

    console.log('🦊 Elysia are listening on port 8080...');
  })
  .catch((err) => {
    console.error('Failed to connect to MongoDB', err);
  });

業務代碼寫好了,接下來我們就要把這個 backend 模塊打成一個鏡像,然後在 Docker Compose 中啓動起來。

首先寫一個 Dockerfile:

# 使用最新版本的 oven/bun 作為基礎鏡像
FROM oven/bun:latest

# 設置工作目錄為 /backend
WORKDIR /backend

# 創建一個空的腳本文件 /bin/run-backend.sh
RUN touch /bin/run-backend.sh

# 賦予 /bin/run-backend.sh 可執行權限
RUN chmod +x /bin/run-backend.sh

# 將安裝依賴和啓動開發服務器的命令寫入 /bin/run-backend.sh
RUN echo "bun install --no-save;" \
     "bun dev --host" \ >> /bin/run-backend.sh

# 暴露容器的 8080 端口
EXPOSE 8080

# 設置容器啓動時執行的命令
CMD ["/bin/run-backend.sh"]

這裏沒有分別使用 RUN 去執行依賴安裝和啓動,而是把它們先寫到一個 .sh 文件再一起跑的方式,是因為我在實踐中摸到過坑。如果用類似下面的寫法:

RUN bun install

CMD ["bun", "dev", "--host"]

會偶現通過 Docker Compose 啓動時報錯,提示權限問題或者一些依賴包找不到等問題,很奇怪。這裏我沒有深入研究,有相關經驗的同學歡迎一起討論。

接下來在 docker-compose.yml 中把它加進去:

services:
  my-mongo: ...

  my-backend:
    image: my-backend
    build:
      context: ./backend
      dockerfile: Dockerfile
      no_cache: true
    restart: always
    volumes:
      - ./backend:/backend #為了在 dev 模式下實現熱更新,這裏把本地目錄映射到容器目錄
    depends_on:
      - my-mongo #依賴數據庫

接下來我們重新執行 docker compose up,便可看到後端服務已經正常啓動了。

image.png

四、前端服務

在項目根目錄下,執行

bun create vite

然後按照提示命名為 frontend,並使用你喜歡的技術棧(這裏使用 React)。前端項目代碼很簡單,就不贅述了,這裏只講述關鍵的一些地方。

首先基於 vite 的項目都能支持代碼熱更新,它有一個默認端口,出於習慣我在 frontend/vite.config.ts 裏把它改成了 3000。

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000
  }
})

然後它的 Dockerfile 如下:

FROM oven/bun

WORKDIR /frontend

RUN touch /bin/run-frontend.sh
RUN chmod +x /bin/run-frontend.sh

# vite 必須要添加 --host 參數才能在容器外訪問到 localhost
RUN echo "bun install --no-save;" \
         "bun dev --host" \ >> /bin/run-frontend.sh

EXPOSE 3000

CMD ["/bin/run-frontend.sh"]

和 backend 類似,這裏也使用了把啓動命令寫到一個 .sh 文件後再啓動的方式。

最後在 docker-compose.yml 中把它加進去:

services:
  my-mongo: ...

  my-backend: ...

  my-frontend:
    image: my-frontend
    build:
      context: ./frontend
      dockerfile: Dockerfile
      no_cache: true

    # 這裏把 frontend 目錄做了個映射,以及暴露 3000 端口,都是為了在 vite 開發時實現代碼熱更新。
    volumes:
      - ./frontend:/frontend
    ports:
      - "3000:3000"

通過 docker compose up 啓動後,查看 my-frontend 容器的 log,看到這樣的輸出就證明成功了:

image.png

五、Nginx 路由分發

現在前端服務在 3000 端口,而後端服務在 8080 端口,會引起跨域問題。這時候就輪到 Nginx 出場了。

回到項目根目錄,新建一個 nginx 目錄,然後往裏面添加 nginx.conf 文件:

server {
  listen 80;

  location / {
    proxy_pass http://my-frontend:3000;
  }

  location /api {
    proxy_pass http://my-backend:8080;
  }
}

由於是在 Docker Compose 中啓動,因此這裏直接填寫容器的名稱。接下來給這個 Nginx 新建一個 Dockerfile:

FROM nginx:latest

COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

這裏有個細節,就是本地的配置文件名叫 nginx.conf,但是複製到容器後要命名為 default.conf,不然會不生效(可能和我拉取的 nginx 鏡像版本有關,大家可以自己實操一下試試)。

同樣的,回到 docker-compose.yml 中把 Nginx 加進去:

services:
  my-mongo: ...

  my-backend: ...

  my-frontend: ...

  my-nginx:
    image: my-nginx
    build:
      context: ./nginx
      dockerfile: Dockerfile
      no_cache: true
    depends_on:
      - my-mongo
      - my-backend
      - my-frontend
    ports:
      - "80:80"

到這一步為止,所有的容器已經準備好了。回到 OrbStack,手動停掉和刪除之前的所有容器和鏡像,再手動在項目根目錄下執行一次 docker compose up,相信你會看正在順利運行的四個容器:

image.png

在瀏覽器上訪問 localhost,相信你也會看到想要的效果:

image.png

六、啓動腳本優化

在上一步的最後,我們是手動停掉和刪除之前的所有容器和鏡像再啓動的,這有點不優雅。如果希望在每次重新啓動這套應用之前,都能清理掉上一次的東西就好了。為了實現這個目的,我們可以在項目根目錄下新建一個 dev.sh

#!/bin/bash

# 停止並移除所有與 docker-compose 配置相關的容器、網絡、卷和鏡像
# --rmi local: 移除本地構建的鏡像
# --volumes: 移除與容器相關的卷
# --remove-orphans: 移除未在 docker-compose 文件中定義的容器
docker-compose down --rmi local --volumes --remove-orphans

# 根據 docker-compose 文件啓動容器
docker-compose up

# 強制刪除所有停止的容器
docker container prune -f

最後給這個 dev.sh 添加可執行權限:

chmod +x ./dev.sh

根目錄下執行 ./dev.sh,即可完成該 TodoMVC 項目的一鍵啓動。

七、尾聲

以上的步驟都是基於“開發模式”而來的,由於 Bun 優異的特性,無論是依賴安裝還是應用啓動都非常絲滑,並且能夠享受到代碼熱更新帶來的便捷。如果需要運行“生產模式”,前後端服務都需要修改其 Dockerfile 的內容,Nginx 的配置也要做相應的修改,這些內容就作為思考題留給各位讀者啦!

(完)

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.