前言
從使用 Docker 快速建立 GUI 圖形化的深度學習開發環境這篇文章之後,我們自己土砲幹了一個 Portainer ,可以用來管理 Docker,但是還是沒辦法滿足我們大量建立並管理開發環境的需求。
內容
這篇文章完成的專案在 Dev Dock Manager
在上一篇文章以 Django 建立 Docker GUI 控制界面 中,我們使用 Django 建立了一個 Docker GUI 控制界面
但是還是沒有辦法滿足我們的需求,因爲我們需要一個可以管理多個開發環境的界面
所以上一次,我們只完成了第一階段的目標
這次,我們把完整的流程圖畫出來:
從圖上可以看到第二階段我們需要:
GUI Docker Image
Development Container
GUI Docker Image
的部分,我們已經在之前的文章有介紹了
詳情可參考使用 Docker 快速建立 GUI 圖形化的深度學習開發環境 ,其專案的位置在 Dev Dock
所以,我們要完成的最後一個部分是『開發容器』的管理
Stage II: Container Management 這個階段,我們分爲兩個部分,其中一個是我們要新增的後端 API
另外一個則是爲了要能夠對 container 使用網頁進行遠端操作,我們需要找到 proxy 的工具並且將其整合進來
Proxy 工具 Traefik 在這個專案中,我使用了 traefik 來做為管理各個 container proxy 的工具
原始碼可以參照 docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 version: "3.3" x-common-networks: &common-networks networks: - d-gui-network x-common-extra-hosts: &extra-hosts extra_hosts: - "host.docker.internal:host-gateway" services: backend: <<: [*common-networks , *extra-hosts ] environment: - DOCKER_NETWORK=d-gui-network labels: - "traefik.enable=true" - "traefik.http.routers.backend-root.rule=Path(`/`)" - "traefik.http.routers.backend-root.service=backend-service" - "traefik.http.routers.backend-login.rule=PathPrefix(`/login`)" - "traefik.http.routers.backend-login.service=backend-service" - "traefik.http.routers.backend-dashboard.rule=PathPrefix(`/dashboard`)" - "traefik.http.routers.backend-dashboard.service=backend-service" - "traefik.http.routers.backend-api.rule=PathPrefix(`/api`)" - "traefik.http.routers.backend-api.service=backend-service" - "traefik.http.routers.backend-websocket.rule=PathPrefix(`/ws`)" - "traefik.http.routers.backend-websocket.service=backend-service" - "traefik.http.services.backend-service.loadbalancer.server.port=8000" traefik: <<: [*common-networks ] image: traefik:v3.0 container_name: d-gui-proxy command: - --providers.docker - --providers.docker.exposedByDefault=false - --entrypoints.web.address=:80 - --api.dashboard=true - --api.insecure - --serverstransport.insecureskipverify=true ports: - "8000:80" - "8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock networks: d-gui-network: external: true
我們在 docker-compose.yml
把設定都定義完成之後,我們就可以使用 docker-compose up -d
來啓動這個服務
另外,補充一下上篇文章沒有提到的部分
因爲我們有使用 task queue ,所以需要額外啓動 Django-RQ
根據 Docker 官方建議做法,我是使用 supervisor 來同時管理的兩個服務(Django、Django-RQ)
詳情可見 supervisord.conf
關於 Django 後端在 supervisord 上的設定如下:
1 2 3 4 5 6 7 8 [program:django] command=python manage.py runserver 0.0.0.0:8000 # 啓動 Django 的方式 stdout_logfile=/dev/stdout # 把 stdout 導到容器內的 /dev/stdout stdout_logfile_maxbytes=0 # 不限制 stdout 的大小 stderr_logfile=/dev/stderr # 把 stderr 導到容器內的 /dev/stderr stderr_logfile_maxbytes=0 # 不限制 stderr 的大小 autostart=true # 自動啓動 autorestart=true # 自動重啓
新增的額外後端 API
在上篇文章中,我們建立 container 的 API 不需要再去做額外的設定
但因爲我們要建立開發用的 container,所以我們需要做帳號、密碼以及環境的設定
原本的 run_image_task
程式碼改成下面這樣:
原始碼可以參照 task.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @job def run_image_task ( image_name, ports, volumes, environment, name, privileged=False , nvdocker=False ): client = docker.from_env() device_requests = [] network = client.networks.get(DOCKER_NETWORK) if nvdocker: device_requests += [ docker.types.DeviceRequest( count=-1 , capabilities=[['gpu' ]], driver='nvidia' ) ] traefik_labels = { "traefik.enable" : "true" , f"traefik.http.routers.d-gui-{name} .rule" : f"PathPrefix(`/novnc/{name} /`)" , f"traefik.http.services.d-gui-{name} .loadbalancer.server.port" : "6901" , f"traefik.http.middlewares.d-gui-{name} -strip-prefix.stripprefix.prefixes" : f"/novnc/{name} /" , f"traefik.http.routers.d-gui-{name} .middlewares" : f'd-gui-{name} -strip-prefix' , "traefik.docker.network" : DOCKER_NETWORK, } container = client.containers.run( image_name, stdin_open=True , detach=True , tty=True , ports=ports, volumes=volumes, environment=environment, name=name, privileged=privileged, device_requests=device_requests, network=network.id , labels=traefik_labels ) return msg
其中有一個地方寫到 DOCKER_NETWORK
,這個是爲了讓所有的 container 都可以在同一個網路上,這樣我們才能夠使用 traefik 來管理
1 DOCKER_NETWORK = os.environ.get("DOCKER_NETWORK" , "d-gui-network" )
偵測機器上面是否能使用 GPU 這件事,我們可以使用 nvidia-smi
但是我們要怎麼在 Django 後端的 container 內部去偵測呢?答案是做不到!
不僅僅是因爲我們的 container 內沒有 NVIDIA GPU 的 driver,而且我們安裝也會很麻煩
可是這樣就沒辦法偵測了嗎?當然不是!
我們可以使用 nvidia/cuda
這個 image 來偵測
如果使用 nvidia/cuda
這個 image 啓動 container 沒有問題,那就代表這個機器是支援 NVIDIA GPU 的
爲了確保使用者先 pull 下來這個 image,我們可以在 docker-compose.yml
中定義一個 nvidia-cuda
的 container
定義在 nvidia-cuda
1 2 3 4 5 nvidia-cuda: image: nvidia/cuda:11.0.3-base-ubuntu20.04 container_name: d-gui-cuda entrypoint: ["echo" , "CUDA image ready" ]
程式內部的偵測方式就可以使用 Docker SDK 來做了
原始碼可以參照 can_use_nvidia_docker.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import dockerfrom docker.errors import APIError, ContainerErrordef can_use_nvidia_docker () -> bool : client = docker.from_env() test_image = 'nvidia/cuda:11.0.3-base-ubuntu20.04' test_command = 'nvidia-smi' try : container = client.containers.run( test_image, command=test_command, runtime='nvidia' , detach=True , auto_remove=True , ) return True except (APIError, ContainerError) as e: print (f"Error checking NVIDIA Docker availability: {e} " ) return False finally : try : container.stop() except Exception: pass
也就是說,我們直接 Run 一個 nvidia/cuda
的 container,然後執行 nvidia-smi
指令
如果成功就代表這個機器是支援 NVIDIA GPU 的,反之則否
對於前端來說,如果機器本身不支援 NVIDIA GPU,我們就可以提示使用者
雖然我們的 VNC、noVNC 可以靠 proxy 來解決訪問的問題
但是 SSH 就不行了,因爲 SSH 是直接連接到 container 的 port
所以我們需要偵測現在能使用哪些 port,然後把這些 port 顯示給使用者選擇或是自動選擇
也就是說,爲了達成這個功能,我們需要做到以下三件事:
判斷某個 port 是否能使用
偵測現在有哪些 port 被 container 使用(!?)
偵測現在有哪些 port 能使用
第一件事,我們可以使用 socket
來做:
原始碼可以參照 check_port_in_use.py
1 2 3 4 5 def check_port_in_use (port ) -> bool : with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(('host.docker.internal' , port)) == 0
上面要注意的是,我們使用 host.docker.internal
來連接到 host 的 gateway
因爲 port 必須對應到主裝置,所以才需要連接到 host
第二件事比較弔詭
因爲理論上來說,我們判斷 port 有沒有被使用,就可以知道哪些 port 可以使用了吧?
但是當 container 是 stop 的狀態下,port 會被釋出,所以我們無法知道哪些 port 是被 container 使用的
所以我們需要做的是,使用 Docker SDK 去判斷哪些 port 是被 container 使用的(包含 stop 的 container)
原始碼可以參照 is_port_used_by_container.py
1 2 3 4 5 6 7 8 def is_port_used_by_container (port:int ) -> bool : client = docker.from_env() for container in client.containers.list (all =True ): port_bindings:dict = client.api.inspect_container(container.id )['HostConfig' ].get('PortBindings' , {}) ports = parse_ports(port_bindings) if str (port) in ports.values(): return True return False
最後一件事!
我們完成前面兩件事之後,就可以從最小的 port 開始往上找,找到沒被使用的 port
這邊要注意的是,找到所有的 port 非常耗時(畢竟是使用 socket 去測試)
所以我們要定義數量,找到足夠的 port 就可以停止了
原始碼可以參照 find_multiple_free_ports.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def find_multiple_free_ports (count ): base_port = 1024 free_ports = [] while len (free_ports) < count and base_port < 65535 : if not check_port_in_use(base_port) and not is_port_used_by_container(base_port): free_ports.append(base_port) base_port += 1 if len (free_ports) < count: raise Exception("Not enough free ports available." ) return free_ports
再寫一個 API 包起來,就可以在前端使用了
從網站就可以看到每次建立 container 會先去問現在有哪些 port 可以使用
結語
這次的專案,我們最後完成了一個多個開發環境的網頁管理系統
內容包含可以操作的 GUI 容器、可網頁控制的 docker 界面以及整個管理系統
也就是一開始的目標:
『以 Django 爲後端開發集成式管理的 Docker 開發環境』
如同一開始這篇文章的 cover 所示,我們可以在這個界面上面建立獨立的開發環境
甚至可以使用網頁進行遠端 GUI 操作以及 web terminal
還可以綁定 NVIDIA GPU,讓開發環境可以使用 GPU 進行各種深度學習 AI 相關的開發及測試
說實在的,其實一開始沒想過要做完整套管理系統
但後來要測試的專案跟環境越來越多又越來越亂,也沒有一個好的管理方式
而且待過的公司都是在同一台機器上面開發(大家甚至都是用 root 帳號,動不動就破壞環境)
還是我沒有去過有好開發方式的公司?
總之,我設想一個系統能夠做到管理不同虛擬環境的事情,於是就催生了這個專案
雖然這個專案的功能還有很多可以改進的地方,但畢竟時間不多,而且也只是 side project
之前能重用的部分我也盡量重用了 XDD
最後,我們來看一下這個專案的部分畫面
登入畫面
首頁看到的 container list
NoVNC 進入 container
Web Terminal
歡迎大家提出建議或是發 PR,我都會處理的 :D
這篇文章同步發表於 Medium ,歡迎留言討論!
Medium 文章連結
Gitalking ...