This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
README.md
|
||||||
|
start.sh
|
||||||
|
start.ps1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Validate and publish container image
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: harbor.mukan.0am.jp
|
||||||
|
IMAGE_NAME: harbor.mukan.0am.jp/services/email-to-discord
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build test stage
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--target test \
|
||||||
|
--tag "${IMAGE_NAME}:test-${{ gitea.sha }}" \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Build runtime image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--target runtime \
|
||||||
|
--tag "${IMAGE_NAME}:${{ gitea.sha }}" \
|
||||||
|
--tag "${IMAGE_NAME}:latest" \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Log in to Harbor
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
|
env:
|
||||||
|
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "${HARBOR_PASSWORD}" | docker login "${REGISTRY}" \
|
||||||
|
--username "${HARBOR_USERNAME}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Push runtime image
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
docker push "${IMAGE_NAME}:${{ gitea.sha }}"
|
||||||
|
docker push "${IMAGE_NAME}:latest"
|
||||||
+14
-5
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim AS base
|
||||||
|
|
||||||
# 作業ディレクトリを設定
|
# 作業ディレクトリを設定
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -15,14 +15,23 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
# アプリケーションファイルをコピー
|
# アプリケーションファイルをコピー
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
|
||||||
|
# テスト用ステージ(CIではこのステージまでビルドして単体テストを実行)
|
||||||
|
FROM base AS test
|
||||||
|
COPY docker-compose.yml docker-compose.local.yml ./
|
||||||
|
COPY tests ./tests
|
||||||
|
RUN python -m unittest discover -s tests -v
|
||||||
|
|
||||||
|
# 実行用ステージ
|
||||||
|
FROM base AS runtime
|
||||||
|
|
||||||
# 非rootユーザーを作成
|
# 非rootユーザーを作成
|
||||||
RUN useradd --create-home --shell /bin/bash appuser
|
RUN useradd --create-home --shell /bin/bash appuser \
|
||||||
RUN chown -R appuser:appuser /app
|
&& chown -R appuser:appuser /app
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
# ヘルスチェックを追加
|
# ヘルスチェックを追加
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import sys; sys.exit(0)"
|
CMD python -c "from pathlib import Path; import sys; sys.exit(0 if b'app.py' in Path('/proc/1/cmdline').read_bytes() else 1)"
|
||||||
|
|
||||||
# アプリケーションを実行
|
# アプリケーションを実行
|
||||||
CMD ["python", "app.py"]
|
CMD ["python", "app.py"]
|
||||||
|
|||||||
@@ -80,7 +80,46 @@ cp .env.example .env
|
|||||||
# .envファイルを編集して実際の値を設定
|
# .envファイルを編集して実際の値を設定
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🐳 Docker Composeでの実行
|
## 🚢 Agents `container_tools` へのデプロイ
|
||||||
|
|
||||||
|
`container_tools` はイメージをビルドせず、レジストリからpullして起動します。このリポジトリでは、`main` へのpush時にGitea Actionsが次のイメージをHarborへ公開します。
|
||||||
|
|
||||||
|
```text
|
||||||
|
harbor.mukan.0am.jp/services/email-to-discord:<commit-sha>
|
||||||
|
harbor.mukan.0am.jp/services/email-to-discord:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Gitea Actions Secretsの設定
|
||||||
|
|
||||||
|
リポジトリのActions Secretsへ次の2項目を登録します。実際の値をリポジトリやComposeへコミットしないでください。
|
||||||
|
|
||||||
|
- `HARBOR_USERNAME`
|
||||||
|
- `HARBOR_PASSWORD`
|
||||||
|
|
||||||
|
Pull Requestではテストステージと実行イメージのビルドだけを行い、`main` へマージされたときだけHarborへpushします。
|
||||||
|
|
||||||
|
### 2. デプロイ用Composeの準備
|
||||||
|
|
||||||
|
通常の [`docker-compose.yml`](docker-compose.yml) は `container_tools` 用テンプレートです。次の値を実際のデプロイ情報へ置き換えます。
|
||||||
|
|
||||||
|
- イメージタグの `latest`(可能ならGitea Actionsが公開したコミットSHAへ変更)
|
||||||
|
- `REPLACE_WITH_EMAIL_USER`
|
||||||
|
- `REPLACE_WITH_EMAIL_PASSWORD`
|
||||||
|
- `REPLACE_WITH_WEBHOOK_URL`
|
||||||
|
- `REPLACE_WITH_WEBHOOK_TOKEN`
|
||||||
|
- `REPLACE_WITH_DISCORD_CHANNEL_ID`
|
||||||
|
|
||||||
|
Discordへの直接通知も併用する場合だけ、空の `DISCORD_WEBHOOK_URL` に実際のURLを設定します。`WEBHOOK_SECRET` は設定しません。
|
||||||
|
|
||||||
|
Agentsの実行時情報にある `WEBHOOK_URL` が `http://127.0.0.1:<port>/webhook` の場合、コンテナ内の `127.0.0.1` はサテライト自身を指すため、`http://host.docker.internal:<port>/webhook` に置き換えてください。ComposeにはLinuxからDockerホストへ到達するための `host-gateway` を設定済みです。
|
||||||
|
|
||||||
|
### 3. デプロイ
|
||||||
|
|
||||||
|
置換後のCompose全文を、安定した `project_name` とともに `container_tools` へ渡します。デプロイ用Composeには、`container_tools` が禁止する `build`、`container_name`、`env_file`、host bind mountを含めていません。
|
||||||
|
|
||||||
|
## 🐳 ローカルDocker Composeでの実行
|
||||||
|
|
||||||
|
ローカルでソースからビルドする場合は、デプロイ用とは別の `docker-compose.local.yml` を使用します。
|
||||||
|
|
||||||
### 1. `.env` の設定
|
### 1. `.env` の設定
|
||||||
|
|
||||||
@@ -101,13 +140,13 @@ DISCORD_CHANNEL_ID=123456789012345678
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# コンテナをビルドして起動
|
# コンテナをビルドして起動
|
||||||
docker-compose up -d
|
docker compose -f docker-compose.local.yml up --build -d
|
||||||
|
|
||||||
# ログの確認
|
# ログの確認
|
||||||
docker-compose logs -f
|
docker compose -f docker-compose.local.yml logs -f
|
||||||
|
|
||||||
# コンテナの停止
|
# コンテナの停止
|
||||||
docker-compose down
|
docker compose -f docker-compose.local.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Linux用シェルスクリプトでの実行
|
## 🚀 Linux用シェルスクリプトでの実行
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
email-to-discord:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runtime
|
||||||
|
container_name: email-to-discord-monitor
|
||||||
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
+16
-10
@@ -1,21 +1,27 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
email-to-discord:
|
email-to-discord:
|
||||||
build: .
|
image: harbor.mukan.0am.jp/services/email-to-discord:latest
|
||||||
container_name: email-to-discord-monitor
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
extra_hosts:
|
||||||
- .env
|
- "host.docker.internal:host-gateway"
|
||||||
|
environment:
|
||||||
# ログの設定
|
IMAP_SERVER: imap.gmail.com
|
||||||
|
IMAP_PORT: "993"
|
||||||
|
USE_SSL: "true"
|
||||||
|
MAILBOX: INBOX
|
||||||
|
EMAIL_USER: REPLACE_WITH_EMAIL_USER
|
||||||
|
EMAIL_PASSWORD: REPLACE_WITH_EMAIL_PASSWORD
|
||||||
|
DISCORD_WEBHOOK_URL: ""
|
||||||
|
WEBHOOK_URL: REPLACE_WITH_WEBHOOK_URL
|
||||||
|
WEBHOOK_TOKEN: REPLACE_WITH_WEBHOOK_TOKEN
|
||||||
|
DISCORD_CHANNEL_ID: "REPLACE_WITH_DISCORD_CHANNEL_ID"
|
||||||
|
WEBHOOK_SOURCE: email-monitor
|
||||||
|
CHECK_INTERVAL: "60"
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
# リソース制限
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function Invoke-Cleanup {
|
|||||||
# docker-compose で起動
|
# docker-compose で起動
|
||||||
function Start-Compose {
|
function Start-Compose {
|
||||||
Log-Info "docker-compose でサービスを起動しています..."
|
Log-Info "docker-compose でサービスを起動しています..."
|
||||||
docker-compose up -d
|
docker-compose -f docker-compose.local.yml up --build -d
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
Log-Info "サービスが起動しました"
|
Log-Info "サービスが起動しました"
|
||||||
Log-Blue "ログを確認するには: .\start.ps1 compose-logs"
|
Log-Blue "ログを確認するには: .\start.ps1 compose-logs"
|
||||||
@@ -238,7 +238,7 @@ function Start-Compose {
|
|||||||
# docker-compose で停止
|
# docker-compose で停止
|
||||||
function Stop-Compose {
|
function Stop-Compose {
|
||||||
Log-Info "docker-compose でサービスを停止しています..."
|
Log-Info "docker-compose でサービスを停止しています..."
|
||||||
docker-compose down
|
docker-compose -f docker-compose.local.yml down
|
||||||
Log-Info "サービスを停止しました"
|
Log-Info "サービスを停止しました"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ switch ($Action.ToLower()) {
|
|||||||
Stop-Compose
|
Stop-Compose
|
||||||
}
|
}
|
||||||
"compose-logs" {
|
"compose-logs" {
|
||||||
docker-compose logs -f
|
docker-compose -f docker-compose.local.yml logs -f
|
||||||
}
|
}
|
||||||
"python" {
|
"python" {
|
||||||
Start-Python
|
Start-Python
|
||||||
|
|||||||
@@ -230,9 +230,9 @@ compose_up() {
|
|||||||
log_info "docker-compose でサービスを起動しています..."
|
log_info "docker-compose でサービスを起動しています..."
|
||||||
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
if command -v docker-compose &> /dev/null; then
|
||||||
docker-compose up -d
|
docker-compose -f docker-compose.local.yml up --build -d
|
||||||
else
|
else
|
||||||
docker compose up -d
|
docker compose -f docker-compose.local.yml up --build -d
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "サービスが起動しました"
|
log_info "サービスが起動しました"
|
||||||
@@ -244,9 +244,9 @@ compose_down() {
|
|||||||
log_info "docker-compose でサービスを停止しています..."
|
log_info "docker-compose でサービスを停止しています..."
|
||||||
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
if command -v docker-compose &> /dev/null; then
|
||||||
docker-compose down
|
docker-compose -f docker-compose.local.yml down
|
||||||
else
|
else
|
||||||
docker compose down
|
docker compose -f docker-compose.local.yml down
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "サービスを停止しました"
|
log_info "サービスを停止しました"
|
||||||
@@ -327,9 +327,9 @@ main() {
|
|||||||
;;
|
;;
|
||||||
"compose-logs")
|
"compose-logs")
|
||||||
if command -v docker-compose &> /dev/null; then
|
if command -v docker-compose &> /dev/null; then
|
||||||
docker-compose logs -f
|
docker-compose -f docker-compose.local.yml logs -f
|
||||||
else
|
else
|
||||||
docker compose logs -f
|
docker compose -f docker-compose.local.yml logs -f
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"python")
|
"python")
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerToolsDeploymentTests(unittest.TestCase):
|
||||||
|
def test_deployment_compose_uses_prebuilt_image_only(self):
|
||||||
|
compose = (REPOSITORY_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"image: harbor.mukan.0am.jp/services/email-to-discord:latest",
|
||||||
|
compose,
|
||||||
|
)
|
||||||
|
self.assertIn("host.docker.internal:host-gateway", compose)
|
||||||
|
for forbidden_key in ("build", "container_name", "env_file"):
|
||||||
|
self.assertIsNone(
|
||||||
|
re.search(rf"^\s+{forbidden_key}:", compose, flags=re.MULTILINE),
|
||||||
|
f"container_toolsで禁止されている'{forbidden_key}'が含まれています",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_local_compose_keeps_source_build_support(self):
|
||||||
|
compose = (REPOSITORY_ROOT / "docker-compose.local.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRegex(compose, r"(?m)^\s+build:")
|
||||||
|
self.assertIn("target: runtime", compose)
|
||||||
|
self.assertIn("host.docker.internal:host-gateway", compose)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user