Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf082d04e4 |
@@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.impeccable
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*.xls
|
||||||
|
*.xlsx
|
||||||
|
screenshots
|
||||||
|
tests
|
||||||
|
docs
|
||||||
|
design-system
|
||||||
|
流水模板
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
APP_PORT=4173
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONPATH=/app/src \
|
||||||
|
APP_HOST=0.0.0.0 \
|
||||||
|
APP_PORT=4173
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN python -m pip install --no-cache-dir --requirement requirements.txt
|
||||||
|
|
||||||
|
COPY server.py ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY web ./web
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 4173
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 \
|
||||||
|
CMD ["python", "-c", "import os, urllib.request; port = os.getenv('APP_PORT', '4173'); urllib.request.urlopen(f'http://127.0.0.1:{port}/', timeout=2)"]
|
||||||
|
|
||||||
|
CMD ["python", "server.py"]
|
||||||
@@ -24,6 +24,24 @@ python -m unittest discover -s tests -v
|
|||||||
python server.py
|
python server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker 部署
|
||||||
|
|
||||||
|
Windows PowerShell 一键启动:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\start-docker.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux 或 macOS 一键启动:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./start-docker.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
默认访问 `http://localhost:4173/`。如需更换宿主机端口,可运行
|
||||||
|
`.\start-docker.ps1 -Port 8080`,或运行 `APP_PORT=8080 ./start-docker.sh`。
|
||||||
|
停止服务使用 `docker compose down`,查看日志使用 `docker compose logs -f app`。
|
||||||
|
|
||||||
访问地址:
|
访问地址:
|
||||||
|
|
||||||
- 登录入口:`http://127.0.0.1:4173/`
|
- 登录入口:`http://127.0.0.1:4173/`
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: caiwuzongzhang:local
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-4173}:4173"
|
||||||
|
environment:
|
||||||
|
APP_HOST: 0.0.0.0
|
||||||
|
APP_PORT: 4173
|
||||||
|
init: true
|
||||||
|
restart: unless-stopped
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -106,9 +106,10 @@ class AppHandler(SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
host = os.environ.get("APP_HOST", "127.0.0.1")
|
||||||
port = int(os.environ.get("APP_PORT", "4173"))
|
port = int(os.environ.get("APP_PORT", "4173"))
|
||||||
server = ThreadingHTTPServer(("127.0.0.1", port), AppHandler)
|
server = ThreadingHTTPServer((host, port), AppHandler)
|
||||||
print(f"Serving on http://127.0.0.1:{port}")
|
print(f"Serving on http://{host}:{port}")
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
param(
|
||||||
|
[ValidateRange(1, 65535)]
|
||||||
|
[int]$Port = 4173
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
docker compose version | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker Compose is not available."
|
||||||
|
}
|
||||||
|
|
||||||
|
docker info | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker is not running."
|
||||||
|
}
|
||||||
|
|
||||||
|
$env:APP_PORT = $Port.ToString()
|
||||||
|
docker compose up --build --detach
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker Compose failed to start the application."
|
||||||
|
}
|
||||||
|
|
||||||
|
$containerId = (docker compose ps --quiet app).Trim()
|
||||||
|
if (-not $containerId) {
|
||||||
|
throw "The application container was not created."
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
||||||
|
$state = docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' $containerId
|
||||||
|
if ($state -eq "healthy") {
|
||||||
|
Write-Host "Application is ready: http://localhost:$Port"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
if ($state -eq "unhealthy" -or $state -eq "exited" -or $state -eq "dead") {
|
||||||
|
docker compose logs --no-color --tail 100 app
|
||||||
|
throw "The application container entered state: $state"
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
|
||||||
|
docker compose logs --no-color --tail 100 app
|
||||||
|
throw "Timed out waiting for the application health check."
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
APP_PORT="${APP_PORT:-${1:-4173}}"
|
||||||
|
export APP_PORT
|
||||||
|
|
||||||
|
case "$APP_PORT" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "APP_PORT must be an integer between 1 and 65535." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$APP_PORT" -lt 1 ] || [ "$APP_PORT" -gt 65535 ]; then
|
||||||
|
echo "APP_PORT must be an integer between 1 and 65535." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose version >/dev/null
|
||||||
|
docker info >/dev/null
|
||||||
|
docker compose up --build --detach
|
||||||
|
|
||||||
|
container_id="$(docker compose ps --quiet app)"
|
||||||
|
if [ -z "$container_id" ]; then
|
||||||
|
echo "The application container was not created." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
while [ "$attempt" -lt 60 ]; do
|
||||||
|
state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
|
||||||
|
case "$state" in
|
||||||
|
healthy)
|
||||||
|
echo "Application is ready: http://localhost:${APP_PORT}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
unhealthy|exited|dead)
|
||||||
|
docker compose logs --no-color --tail 100 app
|
||||||
|
echo "The application container entered state: $state" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
docker compose logs --no-color --tail 100 app
|
||||||
|
echo "Timed out waiting for the application health check." >&2
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import server
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConfigurationTests(TestCase):
|
||||||
|
@patch.dict(os.environ, {"APP_HOST": "0.0.0.0", "APP_PORT": "8080"})
|
||||||
|
@patch("server.ThreadingHTTPServer")
|
||||||
|
def test_main_uses_configured_host_and_port(self, http_server) -> None:
|
||||||
|
server.main()
|
||||||
|
|
||||||
|
http_server.assert_called_once_with(("0.0.0.0", 8080), server.AppHandler)
|
||||||
|
http_server.return_value.serve_forever.assert_called_once_with()
|
||||||
Reference in New Issue
Block a user