Docker Compose Watch 開發熱重載

Docker Compose Watch Hot Reload for Development

Docker Compose Watch 簡介

Docker Compose Watch 是 Docker Compose 2.22.0 版本引入的強大功能,專為提升開發效率而設計。它能夠監控本地檔案系統的變更,並自動將變更同步到運行中的容器,實現真正的熱重載(Hot Reload)開發體驗。

為什麼需要 Docker Compose Watch?

在容器化開發環境中,開發者經常面臨以下困擾:

  • 頻繁重建容器:每次程式碼變更都需要重新執行 docker compose up --build
  • 開發週期緩慢:等待容器重建和重啟消耗大量時間
  • Volume 掛載的限制:傳統 bind mount 在某些場景下效能不佳或配置複雜
  • 環境不一致:開發和生產環境的配置差異可能導致問題

Docker Compose Watch 解決了這些問題,讓您可以:

  1. 即時同步:檔案變更即時反映到容器中
  2. 智能重載:根據變更類型自動選擇適當的更新策略
  3. 零配置啟動:簡單的 YAML 配置即可啟用
  4. 保持環境一致:在接近生產環境的容器中進行開發

與傳統開發模式比較

傳統 Volume 掛載方式

1
2
3
4
5
services:
  web:
    build: .
    volumes:
      - ./src:/app/src

傳統方式的缺點:

  • 需要手動配置 volume 路徑
  • 某些檔案變更(如 package.json)需要手動重建
  • Windows/macOS 上的檔案系統效能問題
  • 無法區分不同類型檔案的處理方式

Docker Compose Watch 方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  web:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json

Watch 方式的優點:

  • 明確的檔案監控規則
  • 自動化的重建和重啟機制
  • 更好的效能和可控性
  • 支援多種更新動作

功能對比表

特性Volume 掛載Docker Compose Watch
檔案同步即時(bind mount)即時(檔案複製)
依賴變更處理手動重建自動重建
配置變更處理手動重啟自動重啟
效能(跨平台)較差優秀
配置複雜度簡單中等
精細控制有限完整

Watch 模式設定語法

基本語法結構

Watch 配置位於 develop 區塊下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  service_name:
    build: .
    develop:
      watch:
        - action: sync
          path: ./local_path
          target: /container_path
          ignore:
            - node_modules/
        - action: rebuild
          path: ./requirements.txt

配置項說明

配置項必要性說明
action必要變更時執行的動作(sync/rebuild/sync+restart)
path必要監控的本地路徑(相對於 Compose 檔案)
targetsync 時必要容器內的目標路徑
ignore選填忽略的檔案或目錄列表

啟動 Watch 模式

1
2
3
4
5
6
7
8
# 啟動 watch 模式
docker compose watch

# 背景執行並監控
docker compose up --watch

# 搭配其他參數
docker compose up -d --watch --build

sync、rebuild、sync+restart 動作差異

Docker Compose Watch 提供三種不同的動作類型,適用於不同的使用場景:

1. sync - 檔案同步

sync 動作會將變更的檔案直接複製到運行中的容器,不會重啟容器或服務。

適用場景

  • 前端靜態資源(HTML、CSS、JavaScript)
  • 支援熱重載的框架(React、Vue、Next.js)
  • 模板檔案
  • 設定檔(不需重啟即可生效)

配置範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  frontend:
    build: ./frontend
    develop:
      watch:
        - action: sync
          path: ./frontend/src
          target: /app/src
          ignore:
            - "**/*.test.js"

運作流程

1
檔案變更 → 偵測變更 → 複製到容器 → 完成

2. rebuild - 完整重建

rebuild 動作會觸發容器的完整重建流程,包括重新執行 Dockerfile 的建構步驟。

適用場景

  • 依賴套件變更(package.json、requirements.txt、go.mod)
  • Dockerfile 變更
  • 編譯型語言的原始碼變更(Go、Rust、C++)
  • 建構配置變更

配置範例

1
2
3
4
5
6
7
8
9
services:
  backend:
    build: ./backend
    develop:
      watch:
        - action: rebuild
          path: ./backend/package.json
        - action: rebuild
          path: ./backend/Dockerfile

運作流程

1
檔案變更 → 偵測變更 → 停止容器 → 重建映像 → 啟動新容器 → 完成

3. sync+restart - 同步並重啟

sync+restart 動作會先同步檔案,然後重啟容器內的服務程序。

適用場景

  • 需要重啟才能載入的設定檔
  • 不支援熱重載的應用程式
  • Python/Node.js 應用程式(非 dev 模式)
  • 資料庫遷移腳本

配置範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
services:
  api:
    build: ./api
    develop:
      watch:
        - action: sync+restart
          path: ./api/config
          target: /app/config
        - action: sync+restart
          path: ./api/src
          target: /app/src

運作流程

1
檔案變更 → 偵測變更 → 複製到容器 → 重啟服務程序 → 完成

動作選擇決策樹

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
檔案變更
    ├── 是否影響建構流程?(依賴、Dockerfile)
    │       │
    │       └── 是 → rebuild
    ├── 應用程式是否支援熱重載?
    │       │
    │       ├── 是 → sync
    │       │
    │       └── 否 → sync+restart
    └── 其他情況 → 根據需求選擇

三種動作的比較

特性syncrebuildsync+restart
執行速度最快最慢中等
需要重建映像
服務中斷短暫
適用情境靜態資源/熱重載依賴變更配置/原始碼

多服務專案設定範例

以下是一個完整的全端應用程式範例,包含前端、後端 API 和資料庫服務。

專案結構

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
my-fullstack-app/
├── docker-compose.yml
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       ├── App.jsx
│       ├── components/
│       └── styles/
├── backend/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── app/
│       ├── main.py
│       ├── routes/
│       └── models/
└── nginx/
    └── nginx.conf

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
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
version: "3.9"

services:
  # PostgreSQL 資料庫
  db:
    image: postgres:15-alpine
    container_name: app_database
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: developer
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U developer -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis 快取
  redis:
    image: redis:7-alpine
    container_name: app_redis
    networks:
      - app_network

  # Python FastAPI 後端
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: app_backend
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://developer:devpassword@db:5432/myapp
      REDIS_URL: redis://redis:6379
      DEBUG: "true"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app_network
    develop:
      watch:
        # Python 原始碼變更 - 同步並重啟
        - action: sync+restart
          path: ./backend/app
          target: /app/app
          ignore:
            - "**/__pycache__"
            - "**/*.pyc"
        # 依賴變更 - 完整重建
        - action: rebuild
          path: ./backend/requirements.txt
        # 靜態設定檔 - 僅同步
        - action: sync
          path: ./backend/static
          target: /app/static

  # React 前端
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      target: development
    container_name: app_frontend
    ports:
      - "3000:3000"
    environment:
      REACT_APP_API_URL: http://localhost:8000
      WATCHPACK_POLLING: "true"
    depends_on:
      - backend
    networks:
      - app_network
    develop:
      watch:
        # React 原始碼 - 利用 HMR 熱重載
        - action: sync
          path: ./frontend/src
          target: /app/src
          ignore:
            - "**/*.test.js"
            - "**/*.test.jsx"
            - "**/__tests__"
        # 公開資源
        - action: sync
          path: ./frontend/public
          target: /app/public
        # 依賴變更 - 重建
        - action: rebuild
          path: ./frontend/package.json
        - action: rebuild
          path: ./frontend/package-lock.json

  # Nginx 反向代理
  nginx:
    image: nginx:alpine
    container_name: app_nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - frontend
      - backend
    networks:
      - app_network
    develop:
      watch:
        # Nginx 配置變更需要重啟
        - action: sync+restart
          path: ./nginx/nginx.conf
          target: /etc/nginx/nginx.conf

networks:
  app_network:
    driver: bridge

volumes:
  postgres_data:

後端 Dockerfile(backend/Dockerfile)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM python:3.11-slim

WORKDIR /app

# 安裝依賴
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 複製應用程式碼
COPY . .

# 開發模式使用 uvicorn 並啟用 reload
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

前端 Dockerfile(frontend/Dockerfile)

 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
# 開發階段
FROM node:20-alpine AS development

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

# 生產階段
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM nginx:alpine AS production

COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

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

啟動開發環境

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 啟動所有服務並開始監控
docker compose up --watch

# 或者背景執行
docker compose up -d --watch

# 查看日誌
docker compose logs -f

# 查看 watch 狀態
docker compose alpha watch

效能優化與忽略規則

忽略規則配置

合理的忽略規則可以顯著提升 Watch 的效能和避免不必要的同步:

 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
services:
  app:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            # 建構產物
            - "**/dist"
            - "**/build"
            - "**/.next"

            # 依賴目錄
            - "**/node_modules"
            - "**/__pycache__"
            - "**/.venv"
            - "**/vendor"

            # 測試檔案
            - "**/*.test.js"
            - "**/*.test.ts"
            - "**/*.spec.js"
            - "**/__tests__"
            - "**/coverage"

            # IDE 和編輯器
            - "**/.idea"
            - "**/.vscode"
            - "**/*.swp"
            - "**/*.swo"
            - "**/*~"

            # 版本控制
            - "**/.git"
            - "**/.gitignore"

            # 日誌和暫存
            - "**/logs"
            - "**/*.log"
            - "**/tmp"
            - "**/.cache"

            # 環境設定
            - "**/.env*"
            - "**/*.local"

Glob 模式語法

模式說明範例匹配
*匹配任意字元(不含路徑分隔符)*.js 匹配 app.js
**匹配任意層級目錄**/test 匹配 src/testlib/test
?匹配單一字元file?.txt 匹配 file1.txt
[abc]匹配括號內任一字元[abc].txt 匹配 a.txt
{a,b}匹配大括號內任一模式*.{js,ts} 匹配 .js.ts

效能優化建議

1. 縮小監控範圍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 不好的做法 - 監控整個專案
develop:
  watch:
    - action: sync
      path: .
      target: /app

# 好的做法 - 只監控需要的目錄
develop:
  watch:
    - action: sync
      path: ./src
      target: /app/src
    - action: sync
      path: ./public
      target: /app/public

2. 分離不同動作的規則

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
develop:
  watch:
    # 高頻變更 - 使用 sync
    - action: sync
      path: ./src/components
      target: /app/src/components

    # 低頻但重要 - 使用 rebuild
    - action: rebuild
      path: ./package.json

    # 配置變更 - 使用 sync+restart
    - action: sync+restart
      path: ./config
      target: /app/config

3. 使用 .dockerignore

確保 .dockerignore 檔案正確配置,減少建構時的檔案傳輸:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env*
*.md
.vscode
.idea
coverage
dist
build

4. 多階段建構優化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 利用 Docker 層級快取
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS development
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["npm", "run", "dev"]

與其他開發工具整合

與 VS Code 整合

Dev Containers 配置

.devcontainer/devcontainer.json 中整合 Docker Compose Watch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "My App Dev Container",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "backend",
  "workspaceFolder": "/app",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-azuretools.vscode-docker"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python"
      }
    }
  },
  "postStartCommand": "docker compose up --watch -d"
}

tasks.json 配置

 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
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Docker Compose Watch",
      "type": "shell",
      "command": "docker compose up --watch",
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "presentation": {
        "reveal": "always",
        "panel": "dedicated"
      },
      "problemMatcher": []
    },
    {
      "label": "Docker Compose Down",
      "type": "shell",
      "command": "docker compose down",
      "problemMatcher": []
    }
  ]
}

與 Make 整合

建立 Makefile 簡化常用操作:

 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
.PHONY: dev watch build down logs clean

# 預設目標
dev: watch

# 啟動 watch 模式
watch:
	docker compose up --watch

# 背景執行 watch
watch-detach:
	docker compose up -d --watch

# 建構映像
build:
	docker compose build

# 停止服務
down:
	docker compose down

# 完整清除
clean:
	docker compose down -v --rmi local

# 查看日誌
logs:
	docker compose logs -f

# 重建並啟動
rebuild: build watch

# 進入後端容器
shell-backend:
	docker compose exec backend /bin/sh

# 進入前端容器
shell-frontend:
	docker compose exec frontend /bin/sh

# 執行測試
test:
	docker compose exec backend pytest
	docker compose exec frontend npm test

# 資料庫遷移
migrate:
	docker compose exec backend alembic upgrade head

與 Git Hooks 整合

使用 pre-commit 確保程式碼品質:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: docker-compose-validate
        name: Validate Docker Compose
        entry: docker compose config -q
        language: system
        files: docker-compose.*\.ya?ml$
        pass_filenames: false

與 CI/CD 整合

GitHub Actions 範例:

 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
# .github/workflows/docker-build.yml
name: Docker Build

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build with Docker Compose
        run: docker compose build

      - name: Run tests
        run: |
          docker compose up -d
          docker compose exec -T backend pytest
          docker compose exec -T frontend npm test
          docker compose down          

最佳實務與常見問題

最佳實務

1. 根據服務特性選擇動作

 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
services:
  # 支援 HMR 的前端框架 → sync
  react-app:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

  # 需要重新載入的後端 → sync+restart
  flask-api:
    develop:
      watch:
        - action: sync+restart
          path: ./app
          target: /app/app

  # 編譯型語言 → rebuild
  go-service:
    develop:
      watch:
        - action: rebuild
          path: ./cmd
        - action: rebuild
          path: ./internal

2. 分離開發和生產配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# docker-compose.yml (基礎配置)
services:
  app:
    build: .

# docker-compose.dev.yml (開發配置)
services:
  app:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

# docker-compose.prod.yml (生產配置)
services:
  app:
    deploy:
      replicas: 3

使用方式:

1
2
3
4
5
# 開發環境
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --watch

# 生產環境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

3. 善用環境變數

1
2
3
4
5
6
7
8
9
services:
  app:
    build:
      context: .
      args:
        NODE_ENV: development
    environment:
      - DEBUG=${DEBUG:-true}
      - LOG_LEVEL=${LOG_LEVEL:-debug}

4. 實作健康檢查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    develop:
      watch:
        - action: sync+restart
          path: ./app
          target: /app/app

常見問題與解決方案

Q1: Watch 模式沒有偵測到檔案變更

可能原因與解決方案

  1. 路徑配置錯誤

    1
    2
    3
    4
    5
    6
    
    # 確保路徑相對於 docker-compose.yml
    develop:
      watch:
        - action: sync
          path: ./src  # 正確:相對路徑
          target: /app/src
    
  2. 檔案被忽略規則排除

    1
    2
    3
    
    # 檢查 ignore 規則是否過於寬鬆
    ignore:
      - "**/*.js"  # 這會忽略所有 JS 檔案!
    
  3. 檔案系統權限問題

    1
    2
    3
    4
    5
    
    # 確認檔案權限
    ls -la ./src
    
    # 必要時調整權限
    chmod -R 755 ./src
    

Q2: 容器頻繁重建導致效能問題

解決方案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 使用 sync 取代 rebuild(適用時)
develop:
  watch:
    # 改用 sync+restart 而非 rebuild
    - action: sync+restart
      path: ./src
      target: /app/src

    # 只有真正需要時才 rebuild
    - action: rebuild
      path: ./Dockerfile
    - action: rebuild
      path: ./package.json

Q3: Windows/macOS 上的效能問題

解決方案

  1. 使用 Docker Desktop 的最新版本
  2. 啟用 VirtioFS(macOS)或 WSL2(Windows)
  3. 減少監控檔案數量
1
2
3
4
5
6
7
8
9
# 精確指定監控路徑
develop:
  watch:
    - action: sync
      path: ./src/app
      target: /app/src/app
      ignore:
        - "**/*.test.*"
        - "**/fixtures"

Q4: 容器內應用程式沒有自動重新載入

解決方案

確保應用程式配置了正確的開發模式:

1
2
3
4
5
6
7
8
# Python + uvicorn
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]

# Node.js + nodemon
CMD ["npx", "nodemon", "--watch", "src", "src/index.js"]

# Go + air
CMD ["air", "-c", ".air.toml"]

Q5: 如何除錯 Watch 配置問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 驗證 Compose 配置
docker compose config

# 查看詳細日誌
docker compose up --watch --verbose

# 檢查容器內檔案
docker compose exec service_name ls -la /app/src

# 監控 Docker 事件
docker events --filter 'type=container'

Q6: 多人協作時的配置衝突

解決方案:使用 override 檔案

1
2
3
4
5
6
7
8
# docker-compose.override.yml(個人配置,加入 .gitignore)
services:
  app:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
1
2
# .gitignore
docker-compose.override.yml

除錯技巧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 確認 Compose 版本支援 Watch
docker compose version
# 需要 v2.22.0 或更高版本

# 2. 驗證配置語法
docker compose config --format json | jq '.services.app.develop'

# 3. 監控同步狀態
docker compose alpha watch --dry-run

# 4. 檢查容器檔案系統
docker compose exec app find /app -newer /app/marker -type f

# 5. 比較本地和容器內檔案
diff <(cat ./src/app.js) <(docker compose exec app cat /app/src/app.js)

總結

Docker Compose Watch 是現代容器化開發工作流程的重要工具,它解決了傳統開發模式中的諸多痛點:

  1. 提升開發效率:自動化的檔案同步和服務重載,減少手動操作
  2. 靈活的更新策略:sync、rebuild、sync+restart 三種動作適用不同場景
  3. 精細的控制能力:透過 ignore 規則和多規則配置實現精確控制
  4. 跨平台一致性:在 Windows、macOS、Linux 上提供一致的開發體驗
  5. 易於整合:與 VS Code、Make、CI/CD 等工具無縫整合

建議您從小型專案開始嘗試,熟悉各種動作類型的適用場景,逐步將 Docker Compose Watch 整合到您的開發工作流程中。

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy