顯示具有 docker 標籤的文章。 顯示所有文章
顯示具有 docker 標籤的文章。 顯示所有文章

2025/12/27

Setting Up Trusted SSL for Laravel Sail and Docker Environments

平常在本機開發時對應的通常是 http,但如果某些特殊原因,想要使用 https 的話,沒有憑證會看到令人礙眼的警告內容,今天紀錄一下怎麼可以在本地使用自簽憑證讓討厭的 ssl 警告消失,有兩種我目前開發的常用情境:

  1. 用 docker 自架 php + nginx
  2. 使用 laravel sail

安裝使用 mkcert

1. 安裝 mkcert

我們使用 Scoop 作為 Windows 的套件管理工具,打開 PowerShell,執行以下指令:

scoop install mkcert

2. 建立本地 CA (憑證授權單位)

這一步最為關鍵,它會讓你的 Windows 系統信任由 mkcert 簽發的憑證:

mkcert -install

注意:若彈出安全性警告,請點擊「是」。

套用自簽憑證

自架範例

1. 專案結構

├── docker/
│   ├── conf.d/
│   │   └── default.conf  # Nginx 設定
│   └── ssl/              # 存放 mkcert 憑證
├── public/
│   └── index.php             # 測試檔案
└── compose.yml           # Docker 配置

2. 產生憑證

# 建立目錄
mkdir -p docker/nginx/ssl

# 使用 mkcert 生成針對 demo.test 的憑證
# 檔案會直接產生在 docker/nginx/ssl 內
cd docker/nginx/ssl
mkcert demo.test

3. default.conf 配置

server {
    listen 80;
    server_name demo.test;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name demo.test;

    ssl_certificate /etc/nginx/ssl/demo.test.pem;
    ssl_certificate_key /etc/nginx/ssl/demo.test-key.pem;

    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

4. compose.yml

services:
  php:
    image: php:8.4-fpm
    volumes:
      - .:/var/www/html

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - .:/var/www/html
      - ./docker/conf.d:/etc/nginx/conf.d
      - ./docker/nginx/ssl:/etc/nginx/ssl
    depends_on:
      - php

sail 範例

1. 生成專屬憑證

切換到你的 Laravel 專案根目錄,建立存放憑證的資料夾並產生檔案:

mkdir -p docker/ssl
cd docker/ssl
mkcert demo.test

2. 配置 Docker 環境

我們將使用 Caddy 作為反向代理伺服器。
修改 docker-compose.yml
找到 services 區塊,加入 caddy 服務,並調整 laravel.test 的埠號對應:

services:
    laravel.test:
        # ... (其餘配置保持不變)
        ports:
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' # 移除 80 埠映射,改由 Caddy 處理
        networks:
            - sail

    caddy:
        image: caddy:latest
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - ./Caddyfile:/etc/caddy/Caddyfile
            - ./docker/ssl:/etc/caddy/certs
            - sail-caddy-data:/data
            - sail-caddy-config:/config
        networks:
            - sail
        depends_on:
            - laravel.test

# 記得在最下方的 volumes 區塊新增
volumes:
    sail-caddy-data:
        driver: local
    sail-caddy-config:
        driver: local

3. 建立 Caddyfile

在專案根目錄建立 Caddyfile:

demo.test {
    reverse_proxy laravel.test:80
    tls /etc/caddy/certs/demo.test.pem /etc/caddy/certs/demo.test-key.pem
}

4. 調整應用程式設定

1. 更新 .env

確保 Laravel 知道目前的通訊協定:

APP_URL=https://demo.test
2. 讓 Laravel 信任 Proxy

修改 bootstrap/app.php,讓 route() 函式能正確生成 https 連結:

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})
3. 配置 Vite (若有使用)

修改 vite.config.js 以支援 HMR (熱重載) 透過 HTTPS 運作:

import fs from 'fs';

export default defineConfig({
    server: {
        host: '0.0.0.0',
        hmr: { host: 'demo.test' },
        https: {
            key: fs.readFileSync('./docker/ssl/demo.test-key.pem'),
            cert: fs.readFileSync('./docker/ssl/demo.test.pem'),
        },
    },
    // ...
});

2025/12/20

Permanently Set Docker Socket Permissions to 666

從之前的 Vagrant 改到使用 wsl + docker 後,PhpStorm 的開發會連到 wsl 裡面的 Docker image 測試,速度還可以接受,Docker 預設情況下,/var/run/docker.sock 的權限為 660(srw-rw----),只有 root 用戶和 docker 組成員才能訪問。如果需要讓所有用戶都能訪問 Docker socket,需要將權限改為 666,通過 systemd 的 override 機制來永久設置 Docker socket 的權限。

步驟 1:創建 systemd 覆蓋配置

sudo mkdir -p /etc/systemd/system/docker.socket.d

步驟 2:創建配置文件

sudo tee /etc/systemd/system/docker.socket.d/override.conf > /dev/null << 'EOF'
[Socket]
SocketMode=0666
EOF

或者手動創建文件 /etc/systemd/system/docker.socket.d/override.conf,內容如下:

[Socket]
SocketMode=0666

步驟 3:應用配置

sudo systemctl daemon-reload
sudo systemctl restart docker.socket docker.service

步驟 4:驗證權限

ls -la /var/run/docker.sock

應該看到輸出類似:

srw-rw-rw- 1 root docker 0 Dec 20 00:15 /var/run/docker.sock

原理說明

  • Docker 的 socket 是由 systemd 的 docker.socket 單元管理的
  • 通過在 /etc/systemd/system/docker.socket.d/ 目錄下創建 override 配置文件,可以覆蓋默認的 socket 設置
  • SocketMode=0666 指定 socket 文件的權限為 666(所有用戶可讀寫)
  • 每次 Docker 啟動時,systemd 會自動應用這個權限設置

安全注意事項

⚠️ 警告:將 Docker socket 權限設置為 666 意味著系統上的任何用戶都可以訪問 Docker,這等同於給予所有用戶 root 權限。請確保你了解這樣做的安全風險:

  • 任何用戶都可以運行容器
  • 可以掛載宿主機的任何目錄
  • 可以以 root 權限執行命令

建議僅在開發環境或你完全信任所有用戶的環境中使用此配置。

配置文件位置

  • 配置文件:/etc/systemd/system/docker.socket.d/override.conf
  • Socket 文件:/var/run/docker.sock

恢復默認權限

如果需要恢復默認的 660 權限,刪除 override 配置文件並重啟服務:

sudo rm /etc/systemd/system/docker.socket.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart docker.socket docker.service

2025/10/08

Laravel Sail On WSL

緣由

我開發 PHP,不管是純 PHP 或 Laravel 都很喜歡用 Homestead,他優點是可以起一台 VM 就可以同時使用多個專案,而且透過修改一個簡單的 Homestead.yml 就可以做完全部環境的設定,但官方已經宣布不維護了,身為資訊人,你要嘛 fork 一份自己維護,不然就要跟著時代走。

不維護的原因也很簡單,VM 真的很肥很吃資源,現在 docker 實在是太香了,而且 Windows 引入 WSL 以後,Docker Desktop 也是秒級啟動,Laravel Sail 也是 base on Docker Compose,是時候跟 Homestead 分手了 。°(°¯᷄◠¯᷅°)°。。

SMB

WSL 有一個痛點,你用他預設掛載進去的目錄執行任何程式碼都慢到哭,目前我實作比較好的方式是透過 SMB 把你的工作目錄掛進去,跑起來跟在本機執行相差無幾,首先是在你要分享的目錄上按右鍵選內容,共用 -> 進階共用 -> 共用此資料夾,共用名稱建議字尾加個 $,這樣在網路芳鄰上會看不到這個目錄,像是 code$,這樣我打 \\HOST\\code$ 可以連到,在公網上是看不到的,另外是我在本機多設定了一組 smb/smb 帳號,這樣我在 demo 的時候也不會因為被看到密碼尷尬,SMB 沒設定的話預設是你這台 Windows 的登入帳號密碼。

WSL

我並沒有使用 Docker Desktop,因為他開機即啟動,而且無法手動 terminate,WSL 資源雖然吃不多,但我沒有要開發的時候並不需要你來吃我這份資源,我知道網路上有許多改機碼之類的方式可以達成這個目的,但官方沒給的我認為都是邪門歪道,我很不喜歡用非官方的方式解決問題,所以我自己起一台 Ubuntu wsl --install -d Ubuntu --name develop,之後再進去自己安裝 Docker,自由自在~

進去 WSL 以後我建議一律 sudo su,用 root 可以少遇到很多奇奇怪怪的權限問題,再來就是手動掛載 smb 了,我將指令放到 ~/.bashrc,因為 WSL 沒有固定 IP,所以你沒辦法把掛載寫死,要的話也是要寫一堆 script 去判斷東西,我是覺得沒必要。

在 Powershell 內可以執行 Get-SmbShare 得到你 smb 的目錄名稱。

Get-SmbShare

Name   ScopeName Path               Description
----   --------- ----               -----------
ADMIN$ *         C:\WINDOWS         遠端管理
C$     *         C:\                預設共用
code$  *         C:\Users\chan\code
IPC$   *                            遠端 IPC
Users  *         C:\Users

/scripts/mount_smb.sh

_get_windows_ip() {
    # 方法 1:透過 /proc/net/route (最穩定)
    # 取得預設閘道,就是 Windows host IP
    local gateway_ip
    gateway_ip=$(awk '$1 == "00000000" {print strtonum("0x"substr($3,7,2))"."strtonum("0x"substr($3,5,2))"."strtonum("0x"substr($3,3,2))"."strtonum("0x"substr($3,1,2))}' /proc/net/route | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 2:透過 ip route (備選)
    gateway_ip=$(ip route show | awk '/^default via/ {print $3}' | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 3:透過 resolv.conf (Windows 10 相容)
    gateway_ip=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    echo "" >&2
    return 1
}

_mount_smb() {
    local smb_share="$1"
    local mount_point="$2"
    local smb_username="smb"
    local smb_password="smb"
    local smb_host
    
    # 檢查必要參數
    if [[ -z "$smb_share" ]] || [[ -z "$mount_point" ]]; then
        echo "Usage: _mount_smb <share> <mount_point>" >&2
        return 1
    fi
    
    # 檢查是否已掛載
    if mountpoint -q "$mount_point"; then
        echo "Already mounted at $mount_point"
        return 0
    fi
    
    # 確保掛載點存在
    [[ -d "$mount_point" ]] || mkdir -p "$mount_point"
    
    # 動態取得 Windows host IP
    smb_host=$(_get_windows_ip)
    
    if [[ -z "$smb_host" ]]; then
        echo "Error: Cannot determine Windows host IP" >&2
        return 1
    fi
    
    echo "Detected Windows host IP: $smb_host"
    echo "Attempting to mount //$smb_host/$smb_share to $mount_point"
    
    # 測試連線
    if ! timeout 3 bash -c "</dev/tcp/$smb_host/445" 2>/dev/null; then
        echo "Error: Cannot connect to $smb_host:445 (SMB port)" >&2
        return 1
    fi
    
    echo "SMB port is reachable, mounting..."
    
    # 執行掛載 (優先使用 SMB 3.0,失敗則試 2.1)
    if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
        -o username="$smb_username",password="$smb_password",vers=3.0,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
        echo "✓ SMB share mounted at $mount_point"
        return 0
    else
        echo "SMB 3.0 failed, trying SMB 2.1..."
        
        if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
            -o username="$smb_username",password="$smb_password",vers=2.1,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
            echo "✓ SMB share mounted at $mount_point (SMB 2.1)"
            return 0
        fi
        
        echo "✗ Failed to mount SMB share" >&2
        return 1
    fi
}

~/.bashrc

# ~/.bashrc - SMB Mount Configuration with Dynamic Windows IP

source /scripts/mount_smb.sh

# 只在互動式 shell 中執行
if [[ $- == *i* ]] && [[ $SHLVL -eq 1 ]]; then
    _mount_smb "code$" /mnt/code
fi

這段程式碼簡單講就是把我在本機 share 的 \\HOST\\code$ 掛到 ~/mnt/code,需要多掛幾個多執行幾次 _mount_smb 即可,每次進入 WSL 便抓取 Host Gateway IP 來連結,目錄跟檔案權限都設定成 777 避免一堆怪事發生,反正開發機不怕。

實際測試

這邊獻上幾個環境跑 phpunit 測試結果,測試數量 1364:

Host

real 1m 37.34s
real 1m 37.83s
real 1m 36.08s


平均:1m 37.08s

Vagrant + SMB

real 3m58.906s
real 3m32.119s
real 3m17.965s


平均:3m 36.33s

WSL + SMB

real 1m48.497s
real 1m57.464s
real 1m50.013s


平均:1m51.991s

其實在 Vagrant 裡面即便使用 SMB 執行速度還是悲劇,我已經這樣操作很久了,但因為 Homestead 的便利性一直選擇忽視這個問題,反正也不是整天跑 full test,另外有趣的一件事情,在 WSL 裡面使用 docker build 一個 php 環境跑測試,我預計應該要掉個 3% 的速度,因為它是 Host -> WSL -> Docker Volume,多一層轉換理應要消耗吧,但沒想到跑了幾次的結果它居然跟在 Windows Host 跑的時間差不多,php.ini 除了調整 memory_limit = -1 以外都沒動,不確定是 Docker 預設拉取的 OS 是 Alpine Linux 效能很好,還是 PHP 官方的 container php.ini 的配置有優化,總之蠻意外的。

之後預計會使用 WSL + Docker + Laravel/Sail 開發測試專案啦,謝謝你 Homestead。

2025/09/13

Docker Compose

原本以為 docker compose 是 docker-compose 指令官方的套件整合,後來發現其實是全新的東西。

差異說明

  • docker-compose:是原先獨立安裝的 Compose 工具,使用 Python 實作,指令是用 docker-compose(中間有連字號),這是 Docker Compose v1 的常用形式。

  • docker compose:是 Docker Compose v2 版本,整合進 Docker CLI 中,採用 Go 語言重寫,使得 Compose 命令成為 docker 命令的一部分,指令使用空格隔開,即 docker compose,這是官方現階段推薦的新作法,提升整合度與效能。

Compose 檔案名稱的使用

歷史上預設檔名是 docker-compose.yml,由於 docker-compose 指令而來,仍被廣泛使用以確保向後相容。

新推薦的檔名是 compose.yaml(或 compose.yml 同樣被接受),更符合 Compose 規範,且映射到 docker compose 指令。官方文件中多建議使用 compose.yaml 作為現代 Compose 的標準檔名。

也可以用 -f 選項指定自訂檔名,靈活在不同目錄或專案間使用多個 Compose 配置檔。

建議改動

  1. 版本號 version 已經不需要寫
  2. deploy 只在 Swarm 模式才有用
  3. 官方推薦改成 compose.yaml
  4. 支援 profiles(服務分組)
services:
  db:
    image: mysql:8
    profiles: ["dev"]

  redis:
    image: redis:6
    profiles: ["prod", "staging"]
docker compose --profile dev up # 這樣執行只有 profiles 含 dev 的 container 會啟動
  1. 官方不建議用 links,用 depends_on 取代
  2. 多檔案合併,常用於「開發」跟「生產」環境分離

compose.yml

services:
  app:
    image: nginx:alpine
    ports:
      - "80:80"
    environment:
      - APP_ENV=production

compose.override.yaml

services:
  app:
    environment:
      - APP_ENV=development
    ports:
      - "8080:80" # 開發改用 8080 port
    volumes:
      - ./src:/usr/share/nginx/html:ro
docker compose -f compose.yaml -f compose.override.yaml up
  1. docker compose cp 已支援
  2. docker compose config 支援扁平化配置檔顯示
  3. 支援 Include
# compose.yaml
include:
  - ./database/compose.yaml
  - ./monitoring/compose.yaml
  
services:
  web:
    image: nginx
  1. 支援單一 up 或 down service
docker compose up web fpm -d
docker compose down fpm

但有 depends_on 的會被連帶啟動。

2025/05/02

Log Handle In Docker

Docker 現在已經是可以當 production server 用的工具了,來聊一下他對 logs 有哪些配置方式。

根據 docker 官方文件,目前有以下幾個配置方法:

Driver 說明
none 容器沒有可用的日誌,且 docker logs 不會返回任何輸出。
local 日誌以一種為了最小化開銷而設計的自定義格式儲存。
json-file 日誌格式為 JSON。Docker 的預設日誌驅動程式。
syslog 將日誌消息寫入 syslog 設施。主機上必須運行 syslog 守護程序。
journald 將日誌消息寫入 journald。主機上必須運行 journald 守護程序。
gelf 將日誌消息寫入 Graylog 擴展日誌格式 (GELF) 端點,如 Graylog 或 Logstash。
fluentd 將日誌消息寫入 fluentd(轉發輸入)。主機上必須運行 fluentd 守護程序。
awslogs 將日誌消息寫入 Amazon CloudWatch Logs。
splunk 使用 HTTP 事件收集器將日誌消息寫入 Splunk。
etwlogs 將日誌消息寫入 Windows 事件追蹤 (ETW) 事件。僅適用於 Windows 平台。
gcplogs 將日誌消息寫入 Google Cloud Platform (GCP) Logging。

如果你能擁有自己的 log server,會建議把 log 打進 log api server,擁有權限的人就可以在 web tool 上做所有的查詢以及決定資料怎麼保留跟處理,畢竟如果 log 停留在各 server,以現在很多服務走 HA 或是 k8s 情況下,遇到狀況你到底要進哪台服務查詢都會成為問題。

不過現實生活中很多人都只有一台 server,而且也沒有 api log server 這種資源,只是要有 log 即可的話,我們一般會用 json-file 這個選項,這也是 docker 預設的 log driver。

docker-compose json-file 範例

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
        logging:
            driver: json-file
            options:
                max-size: "10m"
                max-file: "3"

這個範例會把 nginx 的 log 儲存到 /var/lib/docker/containers/<container-id>/<container-id>-json.log,當 log 檔案大小超過 10MB 時,docker 會自動將舊的 log 檔案重新命名為 <container-id>-json.log.1,並開始寫入新的 log 檔案,最多會保留 3 個 log 檔案,當你使用指令 docker logs 時,docker 會自動讀取最新的 log 檔案,並顯示在終端機上。

一般 image 如 nginx,他會把 log 設定成 stdout,這樣 docker 才能夠讀取到 log,然後寫入到你設定的 log driver 裡面,選擇 json-file 時 docker 會自動幫你把 stdout 或 stderr 的 log 轉成 json 格式,然後寫入到你設定的 log driver 裡面。

nginx 的話,他的配置會是這樣:

access_log /dev/stdout;
error_log /dev/stderr;

這樣的缺點是,access log 會跟 error log 都寫在同一個檔案裡面,會造成你在查詢 log 的時候不方便,假設我們想要使用 docker,又想要產生跟傳統 web server 一樣的 log,我們可以修改 nginx.conf,讓他把 log 寫到 /var/log/nginx/access.log 和 /var/log/nginx/error.log。

Nginx Config 範例

nginx.conf

http {
    server {
        listen       80;
        server_name  localhost;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
            - ./logs:/var/log/nginx

我們在 /etc/logrotate.d/nginx 新增一個檔案,然後把以下的內容貼上去:

logrotate conf

/var/log/nginx/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec nginx nginx -s reopen
    endlaction
}

docker exec nginx nginx -s reopen 的用意是 logrotate 在 rotate 之後需要呼叫 docker 重新開啟 nginx 的 log,不這樣的話 nginx in docker 會使用你 rename 後的 log 檔案繼續寫 log。

當我們使用 compress,並且會針對壓縮過後的檔案做後續處理的話,一定要有 sharedscripts 以及使用 lastaction,如果使用 postrotate 的話,他不會等所有的檔案都執行完壓縮就直接作動了。

這邊有幾個 logrotate 的指令可以使用:

指令 說明
prerotate/endscript 在進行日誌輪轉之前執行的指令,每個匹配的日誌檔案都會執行一次
postrotate/endscript 在進行日誌輪轉之後執行的指令,每個匹配的日誌檔案都會執行一次
firstaction/endscript 在所有匹配日誌檔案進行輪轉之前執行的指令,只執行一次
lastaction/endscript 在所有匹配日誌檔案進行輪轉之後執行的指令,只執行一次
preremove/endscript 在舊的日誌檔案被移除之前執行的指令

另外有一種不用重新開啟檔案的配置參數 copytruncate,這個參數會在 logrotate 開始 rotate 的時候複製原本的 log 檔案,然後 truncate 原本的 log 檔案,這樣 nginx 就不需要重新開啟檔案了,但這樣有機會漏寫紀錄,所以不建議使用這個參數。

Apache2 Config 範例

apache2 的原始 log 配置會是:

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 common

我們要修改成:

ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    apache2:
        image: httpd
        container_name: httpd
        ports:
            - "80:80"
        volumes:
            - ./httpd.conf:/usr/local/apache2/conf/httpd.conf
            - ./logs:/var/log/apache2

httpd.conf

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot /var/www/html

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
</VirtualHost>

logrotate conf

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec apache2 apachectl -k graceful
    endscript
}

這個範例跟 nginx 的範例一樣,只是把 nginx -s reopen 改成 apachectl -k graceful。

假設你 logrotate 後要執行其他 bash 做其他處理的話。

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec httpd apachectl -k graceful
        /usr/bin/bash /usr/local/bin/other_script.sh
    endscript
}

2025/03/03

Docker PHP Extension Installer

我是一位 PHP 開發者,目前許多的 production 專案也會用到 docker 了,所以套件安裝經常會在 Dockerfile 出現,在 php - Official Image | Docker Hub 上你要安裝 extension,他的範例是:

FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
		libfreetype-dev \
		libjpeg62-turbo-dev \
		libpng-dev \
	&& docker-php-ext-configure gd --with-freetype --with-jpeg \
	&& docker-php-ext-install -j$(nproc) gd

這樣寫蠻醜的也不好維護,後來找到了好用的 mlocati/docker-php-extension-installer: Easily install PHP extensions in Docker containers,安裝方式就變得很乾淨。

FROM php:7.2-cli

RUN curl -sSLf \
        -o /usr/local/bin/install-php-extensions \
        https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
    chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions gd xdebug

在 install-php-extensions 一直增加你要的東西就可以,下方還有各 PHP 有哪些支援的 extension 表格對應,可以安裝 composer 版本也很貼心。

# Install the latest version
install-php-extensions @composer
# Install the latest 1.x version
install-php-extensions @composer-1
# Install a specific version
install-php-extensions @composer-2.0.2

2022/12/05

From Docker Virtual Network Connect To Solid MySQL Server

假設我們的 server 只有一台,PHP 環境為 docker container,而資料庫為實體機安裝,資料庫 3306 防火牆防堵不對外,但要允許從 Docker 環境內的服務存取,最簡單的方式就是防火牆讓 Docker 產生的 network interface 通過,下面我們來實做這件事。

今天示範環境為使用 Vagrant 起一台 Ubuntu jammy64,然後在裡面進行防火牆的測試。

sudo service ufw start
sudo ufw enable
sudo ufw allow ssh

記得開啟防火牆要先把 ssh 打開,不然就悲劇了,接著安裝 MySQL,docker 以及 docker compose 部分就麻煩自行安裝。

sudo apt install -y mariadb-server mariadb-client

安裝完畢後,我們來建立測試用資料庫以及帳號。

sudo mysql
create database laravel;
GRANT ALL PRIVILEGES ON `laravel`.* TO 'homestead'@'%' IDENTIFIED BY 'secret' WITH GRANT OPTION;

這邊權限開 % 沒關係,因為我們只會允許 docker network 指定的 network interface 進入而已。

docker-compose.yml
version: "3.9"

services:
  fpm:
    container_name: fpm
    build: .
    restart: always
    working_dir: /www
    extra_hosts:
      - "host.docker.internal:host-gateway"
    networks:
      - front_end
    volumes:
      - ./www/:/www

networks:
  front_end:
    driver_opts:
      com.docker.network.bridge.name: br-fpm
Dockerfile
FROM php:8.1-fpm

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions @composer pdo_mysql mbstring exif pcntl bcmath gd

WORKDIR /www

這是要安裝 Laravel 的最小環境,我們今天只是要測試連線所以其他的設定就先不理會了。

sudo docker compose up -d

等 image build 完以及 service 起來以後,我們來利用指令安裝一個 Laravel 的環境。

sudo docker compose exec fpm composer create-project laravel/laravel .

接著我們打開 .env 檔案設定資料庫連線內容。

DB_CONNECTION=mysql
DB_HOST=host.docker.internal
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=homestead
DB_PASSWORD=secret

這時來測試一下連線。

sudo docker compose exec fpm php artisan migrate

因為防火牆擋住了,所以 connection 會 hang 住,最終失敗,接著來開始防火牆設定。

sudo ufw allow in on br-fpm to any port 3306

接著再執行一次 migrate 的指令。

sudo docker compose exec fpm php artisan migrate

   INFO  Preparing database.

  Creating migration table ............................................................................................................... 10ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table .................................................................................................... 9ms DONE
  2014_10_12_100000_create_password_resets_table .......................................................................................... 2ms DONE
  2019_08_19_000000_create_failed_jobs_table .............................................................................................. 3ms DONE
  2019_12_14_000001_create_personal_access_tokens_table ................................................................................... 4ms DONE

可以看到 migration 順利成功了,如果發生 connection refuse 的錯誤的話,記得把 MySQL 的 bind-address 設定為 0.0.0.0。

iptable 的話用下方的指令應可達到一樣的效果。

iptables -A INPUT -i br-fpm -p tcp --dport 3306 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP

2022/10/18

Reload Nginx And Apache Config In Docker

如果在本機安裝 apache2 或 nginx 時想要 reload config 的話我們通常會使用下列的指令:

# nginx
nginx -s reload

# apache2
service httpd reload

但在 docker 的環境下就沒有那麼簡單,像 apache 的 reload 是把 script 寫在 service 的 config 裡面,不一定每個 image 裡面都有包含這個指令,CentOS 的我記得好像就沒有,為了要兼容所有的環境,我們可以利用 kill -USR1 這個方式套用到每個我們想要 reload without restart 的專案上面。

apache2

首先在根目錄建立兩個網頁檔:

  • ./www/1/index.html
  • ./www/2/index.html

各別會在網頁印出 1 以及 2 的內容識別。

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: httpd
    image: httpd
    restart: always
    volumes:
      - "./www:/www"
      - "./config/httpd:/usr/local/apache2/conf:ro"

我們建立了一個名為 http 的 container,使用 httpd 的 image,並且把 www 目錄掛進去,我事先把 httpd 的 config 複製出來以便在外面修改內容,接下來跑 docker compose up -d,打上 IP 後可以看到 httpd 傳統的首頁內容 It works!。

接著我們修改 httpd.conf:

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">

# 改為

DocumentRoot "/www/1"
<Directory "/www/1">

我們先使用 docker compose restart 測試,執行後 refresh 網頁可以看到我們要的 1,接著再把 config 改成:

DocumentRoot "/www/2"
<Directory "/www/2">

這次我們使用 docker compose kill -s USR1 http ,此時我們可以看到 container 活得好好的,但 refresh 頁面後得到我們想要的 2 了,他的原理就是呼叫 container 內的 httpd daemon 去重新整理他的 service。

nginx

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: nginx
    image: nginx
    restart: always
    volumes:
      - "./www:/www"
      - "./config/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro"

一樣,我們事前複製了 nginx 的 config 出來重新掛載回去,啟動 container 以後看見 nginx 的預設字樣 Welcome to nginx!。

接著我們修改 defalut.conf

root   /usr/share/nginx/html;

# 改為

root   /www/1;

跟 apache2 一樣我們先使用 docker compose restart,可以順利看到 1 的結果,接著我們將 config 修改為:

root   /www/2;

使用 docker compose exec http nginx -s reload,我們就可以順利看到 2 了,nginx 跟 apache2 的 container 啟動方式不太一樣,所以不能用 kill 的方法,但事實上在 nginx 的 logrotate config 裡面他使用的方式也是:

kill -USR1 `cat /var/run/nginx.pid`

上面介紹了兩個主流網頁伺服器的重取 config 的目的是在當我們把 server 的 log 掛到我們本機目錄時,我們可能會需要依照情況在本機執行 logrotate 的動作,這樣就可以把 reload 腳本寫在本機的 logrotate config 的 postrotate/endscript 裡面了。

這邊附上 logrotate for docker 基本的配置:

nginx-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 52
        compress
        delaycompress
        notifempty
        create 640 nginx adm
        sharedscripts
        postrotate
            sudo docker exec YOUR_CONTAINER_NAME nginx -s reload
        endscript
}

httpd-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        sharedscripts
        postrotate
            sudo docker kill -s USR1 YOUR_CONTAINER_NAME 
        endscript
}

2019/07/23

Apache Docker 權限問題

最近在練習使用 docker 建立產品環境,遇到一個權限問題,container 的內容是隔離的,但我們網站有可能會修修改改,如果每次改好才 cp 進去的話很麻煩,所以我採用的方式是將 apache container 內的 /var/www/ 目錄掛出來,直接在本機編輯檔案,如果是靜態網站的話都還好,如果需要用到寫入權限的時候會發生問題。

本機檔案的權限是 root:root,但 container 內的 apache 的權限是 www-data:www-data,所以透過 apache 執行檔案遇到 root:root 時 755 是沒有作用的,開 777 又不妥,目前綜合網路的解法就是在本機建立一個帳號,確定他的 uid,然後將 container 內的 www-data 帳號 uid 改相同,就可以解決這件事,步驟如下

$ useradd -u 3000 docker -g docker
$ chown docker -R /var/www/

Dockerfile

FROM php-apache

RUN usermod -u 3000 www-data

這樣的話外部的檔案權限就是 docker:root,而 container 內會是 www-data:root,後面 root 可以忽略,寫入權限就完成了。

2019/07/10

Docker Compose Note

Docker Compose Document

version: '3'

services:
    redis:
      image: redis
      container_name: redis
      networks:
        default:
          ipv4_address: '172.18.0.12'
    nginx:
      image: nginx
      container_name: ngx
      volumes:
        - default:/data/
      networks:
        default:
          ipv4_address: '172.18.0.11'
      ports:
        - '8080:80'
      depends_on:
        - redis

networks:
  default:
    external:
      name: chan-network

volumes:
  default:
    external:
      name: chan-volume

2017/02/27

Reverse Proxy For Docker

前言

現在很流行用 docker 處理一些服務,以我這行來講用來處理 LAMP 或者是 LNMP 最常見,也可能是產生個 gitlab 之類的,docker 最大的優點是服務都跑在 container,安裝快啟動也快,要幾個有幾個,寫好 config 像是 Dockerfile 之類的設定還可以一個指令搞定,讓你的電腦可以保持很乾淨,網站使用 80 port 當作預設的入口,但每個 server port 都是唯一的,所以假設你有很多網站服務,可能會指派很多不同的 port,像是 localhost:66666 或者是 localhost:66667,但有些測試你可能想要盡可能逼真,希望是用網址來跑服務的話,我們可以在 80 port 假一個反向的 proxy server,讓你可以將 domain 指定到帶 port 的 IP,下面會示範 nginx 以及 apache 的設定方式

Docker

我測試的環境是 Windows,我先使用 Vagrant 安裝一個 ubuntu 16 的 VM,IP 為 10.10.10.22,之後將會在這個 VM 裡面跑 nginx 以及 apache servie,還有 docker

我 container 使用的 image 是 novice/lnmp,我先啟動兩個 web service 服務

$ sudo docker run -itd --name web1 -p 33333:80 novice/lnmp
$ sudo docker run -itd --name web2 -p 33334:80 novice/lnmp

我將 33333 port 給 nginx 這個 web service 使用,33334 給 apache 使用,然後我們將 Windows 的 hosts 設定一下

10.10.10.22 docker.web1
10.10.10.22 docker.web2

接著我進到 apache container 的 /var/www 目錄,加上一個 hello.php

<?php

echo 'hello I am web1';

同樣在 nginx 的 container /var/www 加入 hello.php。

<?php

echo 'hello I am web2';

在網址輸入 http://docker.web1:33333/hello.php 以及 http://docker.web2:33334/hello.php 各別會看到 hello I am web1 以及 hello I am web2,不過其實目前兩個網址互換得到的結果也會一樣,因為現在主要是因為不同的 port 帶出不同的 web service,而我們要做的事情就是把 port 拿掉以後還會成立

nginx

先在這個 VM 安裝 nginx。

$ sudo apt-get install nginx

安裝好之後,我們在 /etc/nginx/sites-available的目錄建立一個 proxy.conf內容如下

server {
    listen 80;
    sendfile off;
    server_name docker.web1;

    location / {
        proxy_pass http://127.0.0.1:33333;
        proxy_redirect     off;
        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-Host $server_name;
    }
}

server {
    listen 80;
    sendfile off;
    server_name docker.web2;

    location / {
        proxy_pass http://127.0.0.1:33334;
        proxy_redirect     off;
        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-Host $server_name;
    }
}

接著設定 config,然後將服務重啟

$ sudo ln -s /etc/nginx/sites-available/proxy.conf /etc/nginx/sites-enabled/proxy.conf
$ sudo service nginx restart

此時如果你將 port 拿掉還是可以看到正確結果的話表示成功了,說明一下 sendfile off 主要是因為我使用 VM,這個不關的話會一直看到 cache,如果你是直接在 Linux 環境的話應該是不用特別設定

Apache

首先來安裝 apache,記得先將 nginx 關掉,並且確認兩個 proxy 必要的 module 有開

$ sudo service nginx stop
$ sudo apt-get install apache2
$ sudo a2enmod proxy
$ sudo a2enmod proxy_http

接著在 /etc/apache2/sites-available加入 proxy.conf,內容如下

<VirtualHost *:80>
    ServerName docker.web1
    ProxyPass / http://127.0.0.1:33334/
    ProxyPassReverse  / http://127.0.0.1:33334/
</VirtualHost>

<VirtualHost *:80>
    ServerName docker.web2
    ProxyPass / http://127.0.0.1:33333/
    ProxyPassReverse  / http://127.0.0.1:33333/
</VirtualHost>

接著我們製作 link,以及重起 apache

$ sudo a2ensite proxy.conf
$ sudo service apache2 restart

如果那兩個網址重整以後一樣可以看到對應的文字,表示 apache reserse proxy 也成功了

2016/11/30

Docker On Windows

enter image description here

前言

我的工作環境是 Windows,但很習慣使用 command line 工作,雖然 Windows 有一些工具可以模擬,像是 Cygwin,或者是 cmder,但都只能說堪用而已,所以我習慣在 Windows 上安裝 Vagrant 當作工作環境,久仰 docker 大名,最近花了點時間研究,今天這篇文章會一個一個步驟帶到怎麼在 Windows 使用 docker toolbox 安裝一個 PHP7 container by ubuntu xenail 連結另一個 mysql container

開始 Docker 之旅

首先去 docker tool 下載頁面,下載 Windows 安裝檔,之後便是無窮的下一步,這邊介紹一下這個工具的運作方式,他會使用 docker-machine 的 vm 技術透過 virtualbox build 一個虛擬環境讓你跑 docker command,所以安裝成功後會有 Docker Quickstart Terminal 可以用,但他就跟其他模擬 bash 環境的軟體一樣只是堪用而已,所以我打算使用 xshell 去走這個 demo,也可以使用 putty

安裝完成後 docker vm 預設的 ip 是 192.168.99.100,ssh 192.168.99.100 以後會問你帳密,分別是 docker 以及 tcuser,進入以後我們就可以使用所有的 docker command,我今天要使用 ubuntu 跟 mysql/mysql-server 這兩個 container,當然一定也有 build 好的 lamp image,但我打算 step by step 紀錄一下,至於 docker 的一些基礎命令我不會多做解釋,可以去 Docker —— 從入門到實踐 看一下,我們先 pull 一下

$ docker pull ubuntu
$ docker pull mysql/mysql-server

$ docker images

REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
ubuntu               latest              4ca3a192ff2a        12 hours ago        128.2 MB
mysql/mysql-server   latest              6272a6121ff6        7 days ago          316.2 MB

已經順利將這兩個 container 抓下來,先來設定 database

$ docker run --name db1 -e MYSQL_ROOT_PASSWORD=123456 -d mysql/mysql-server
$ docker ps

CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                 NAMES
dfc880ed429d        mysql/mysql-server   "/entrypoint.sh mysql"   18 seconds ago      Up 17 seconds       3306/tcp, 33060/tcp   db1

上述指令建立了一個名叫 db1 的 mysql server,root 密碼為 123456,但預設帳號只有 root@local,所以我們必須建一個 root@% 的帳號來使用,順便建立一個資料表來測試

$ docker exec -it db1 mysql -uroot -p123456
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
create database test;

mysql> use test;

mysql> CREATE TABLE `tests` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;

mysql> INSERT INTO `tests` (`name`) VALUES ('a'), ('b');

mysql> exit;

到此 mysql-server 的部份就完成了,現在來進行 web 的部份

$ docker run -td --name php7 --link db1:db1 ubuntu /bin/bash
$ docker ps

CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                 NAMES
2c52f1c58edb        ubuntu               "/bin/bash"              12 seconds ago      Up 11 seconds                             php7
dfc880ed429d        mysql/mysql-server   "/entrypoint.sh mysql"   10 minutes ago      Up 10 minutes       3306/tcp, 33060/tcp   db1

這邊順利的建立了 php7 這個 container,並且將其 link 到 db1,接著要一連串的安裝 php7 並且寫一個 pdo 測試連線的流程

$ docker exec -it php7 bash

$ env

# 擷取重點
DB1_PORT_3306_TCP_ADDR=172.17.0.2
DB1_PORT_3306_TCP_PROTO=tcp
SHLVL=1
HOME=/root
DB1_ENV_MYSQL_ROOT_PASSWORD=123456
DB1_PORT=tcp://172.17.0.2:3306
DB1_PORT_33060_TCP_PROTO=tcp
DB1_NAME=/php7/db1

$ apt-get udpate
$ apt-get install php php-mysql vim

$ vim test.php

test.php 的內容如下

<?php

$ip = getenv('DB1_PORT_33060_TCP_ADDR');
$dsn = "mysql:host=${ip};dbname=test";
$db = new PDO($dsn, 'root', 123456);
$sql = "SELECT * FROM `tests`";

foreach ($db->query($sql) as $value) {
    var_dump($value);
}

接著我們執行看看

$ php test.php

array(4) {
  ["id"]=>
  string(1) "1"
  [0]=>
  string(1) "1"
  ["name"]=>
  string(1) "a"
  [1]=>
  string(1) "a"
}
array(4) {
  ["id"]=>
  string(1) "2"
  [0]=>
  string(1) "2"
  ["name"]=>
  string(1) "b"
  [1]=>
  string(1) "b"
}

這樣就大功告成啦

docker-machine

docker 一次只能 bind 一個對外的 80 port,如果要實現不同 domain name 不帶 port 去不同的 container 這種作法,前端要架設一個 nginx 去設定導向,這邊提供另一個偷懶的方法,docker-machine 本身用的就是 vm,所以我們可以開另一個 vm,這時候就得進到 docker terminal,執行以下指令

$ docker-machine create -d virtualbox chan

接著執行

$ docker-machine ls

我們會看到

NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER    ERRORS
chan      -        virtualbox   Running   tcp://192.168.99.101:2376           v1.12.3
default   *        virtualbox   Running   tcp://192.168.99.100:2376           v1.12.3

成功的建立了 chan 這個 vm,我們可以使用 ssh 進去 192.168.99.101,如果你在 docker terminal 要切換環境只要執行下方指令即可,然後把 lamp 的環境做在這個 ip 裡面,並且在本機設好 hosts 連過來即可

$ eval $(docker-machine env chan)

可以將自製的 web image 拷貝出來並寫成 Dockerfile,這樣開一個 vm 並且建一個新環境非常的快,如果只是測試環境不在意 port 的話就用不同的 port 去導向每個 container,或者是要用哪個 case 就 start 哪個 container 也是 ok 的,如果你跟我一樣重開機或關機會把軟體都關乾乾淨淨的話,可以在 docker toolbox terminal 下指令

$ docker-machine stop default