ci: single Gitea Actions pipeline, uv-based image, scoped docker cleanup
Replace lint/test/docker workflows with ci.yml (gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push, docs-check, security). Rebuild the Dockerfile as a digest-pinned multi-stage uv build running as uid 1000, add .dockerignore, a hardened compose file and a gitleaks config. The integration harness now falls back to /etc/hostname and /proc/self to find its own container.
This commit is contained in:
1 parent
9e50859133
commit
1a039e32ab
16 files changed
+729
-404
No files matched your search
+51
-34
@@ -1,51 +1,68 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Build stage
|
||||
FROM python:3.12-slim AS builder
|
||||
# Both stages use the same python:3.12-slim image, pinned by digest so a build
|
||||
# is reproducible. Refresh the digest deliberately (docker pull, then
|
||||
# docker inspect --format '{{index .RepoDigests 0}}' python:3.12-slim).
|
||||
ARG PYTHON_IMAGE=python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.12.5@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
FROM ${UV_IMAGE} AS uv
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Builder: resolve the locked, non-dev dependencies and install the project
|
||||
# (non-editable) into /app/.venv. Nothing from this stage except the venv is
|
||||
# copied into the runtime image.
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM ${PYTHON_IMAGE} AS builder
|
||||
|
||||
COPY --from=uv /uv /usr/local/bin/uv
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=never \
|
||||
UV_PROJECT_ENVIRONMENT=/app/.venv
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY pyproject.toml uv.lock README.md LICENSE ./
|
||||
COPY src ./src
|
||||
RUN uv sync --locked --no-dev --no-editable
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime: slim image, only the venv, non-root user.
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM ${PYTHON_IMAGE}
|
||||
|
||||
# Runtime stage
|
||||
FROM python:3.12-slim
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH=/app/.venv/bin:$PATH
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV NODE_ENV=production
|
||||
ENV ENVIRONMENT=production
|
||||
ENV PATH=/home/aegis/.local/bin:$PATH
|
||||
ENV PYTHONPATH=/app/src:$PYTHONPATH
|
||||
# Non-root runtime user (uid 1000). DATA_DIR holds the token store and the
|
||||
# audit log; mount a persistent volume there.
|
||||
RUN useradd --create-home --uid 1000 --shell /usr/sbin/nologin aegis \
|
||||
&& mkdir -p /var/lib/aegis-mcp \
|
||||
&& chown aegis:aegis /var/lib/aegis-mcp
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chown=root:root /app/.venv /app/.venv
|
||||
|
||||
# Non-root runtime user
|
||||
RUN useradd -m -u 1000 -s /usr/sbin/nologin aegis
|
||||
|
||||
COPY --from=builder --chown=aegis:aegis /root/.local /home/aegis/.local
|
||||
COPY --chown=aegis:aegis src/ ./src/
|
||||
COPY --chown=aegis:aegis scripts/ ./scripts/
|
||||
|
||||
# /var/log/aegis-mcp -> audit log (mount a writable volume)
|
||||
# /var/lib/aegis-mcp -> dynamic client registration store (mount a writable, persistent volume)
|
||||
RUN mkdir -p /var/log/aegis-mcp /var/lib/aegis-mcp /tmp/aegis-mcp \
|
||||
&& chown -R aegis:aegis /var/log/aegis-mcp /var/lib/aegis-mcp /tmp/aegis-mcp
|
||||
# Binding 0.0.0.0 is required for the published port to work inside a
|
||||
# container. It is safe by default: the server refuses to start (exit code 2)
|
||||
# on a non-loopback bind unless authentication is configured (MCP_BEARER_TOKEN
|
||||
# or OAUTH_ENABLED). Still publish the port only on 127.0.0.1 or behind a
|
||||
# reverse proxy (see docker-compose.yml).
|
||||
ENV DATA_DIR=/var/lib/aegis-mcp \
|
||||
MCP_TRANSPORT=http \
|
||||
MCP_HOST=0.0.0.0 \
|
||||
MCP_PORT=8080
|
||||
|
||||
USER aegis
|
||||
WORKDIR /home/aegis
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD python -c "import httpx; httpx.get('http://127.0.0.1:8080/health', timeout=5)" || exit 1
|
||||
# Python stdlib only (the slim image has no curl). Probes loopback on MCP_PORT.
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||
CMD ["python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/health' % os.environ.get('MCP_PORT', '8080'), timeout=5)"]
|
||||
|
||||
CMD ["python", "-m", "aegis_gitea_mcp.server"]
|
||||
ENTRYPOINT ["aegis-gitea-mcp"]
|
||||
CMD ["serve"]
|
||||
@@ -1,51 +0,0 @@
|
||||
services:
|
||||
aegis-mcp:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
container_name: aegis-gitea-mcp
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
ENVIRONMENT: production
|
||||
MCP_HOST: ${MCP_HOST:-127.0.0.1}
|
||||
ALLOW_INSECURE_BIND: ${ALLOW_INSECURE_BIND:-false}
|
||||
ports:
|
||||
- "127.0.0.1:${MCP_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- aegis-mcp-logs:/var/log/aegis-mcp
|
||||
- aegis-mcp-data:/var/lib/aegis-mcp
|
||||
- ../policy.yaml:/app/policy.yaml:ro
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
user: "1000:1000"
|
||||
networks:
|
||||
- proxy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"python",
|
||||
"-c",
|
||||
"import httpx; httpx.get('http://127.0.0.1:8080/health', timeout=5)",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
aegis-mcp-logs:
|
||||
driver: local
|
||||
aegis-mcp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
Reference in new issue
Block a user