feat(llm-api): add CI/CD, Dockerfile, compose, Traefik routing
- infra/docker/llm-api.Dockerfile — multi-stage Rust build with cargo-chef - infra/compose/llm-api.yml — service with model volume mount & healthcheck - infra/traefik/dynamic/apps.yaml — router for ai.asepharyana.my.id - CI: build-push workflow detects llm-api changes, builds image - CD: deploy workflow includes llm-api.yml in compose stack
This commit is contained in:
@@ -89,7 +89,7 @@ jobs:
|
|||||||
git fetch origin main --depth=1 || true
|
git fetch origin main --depth=1 || true
|
||||||
|
|
||||||
# Detect changed files before resetting
|
# Detect changed files before resetting
|
||||||
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/elysia.yml infra/compose/react.yml infra/compose/rust-auth.yml"
|
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/elysia.yml infra/compose/react.yml infra/compose/rust-auth.yml infra/compose/llm-api.yml"
|
||||||
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
|
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
|
||||||
|
|
||||||
if git rev-parse HEAD >/dev/null 2>&1; then
|
if git rev-parse HEAD >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ jobs:
|
|||||||
elysia-api: ${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
elysia-api: ${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||||
react-web: ${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
react-web: ${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||||
rust-auth: ${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
rust-auth: ${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
llm-api: ${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
@@ -65,7 +66,10 @@ jobs:
|
|||||||
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||||
echo "elysia-api=$(changed '^(apps/elysia(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/elysia\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
echo "elysia-api=$(changed '^(apps/elysia(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/elysia\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||||
echo "react-web=$(changed '^(apps/react(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/react\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
echo "react-web=$(changed '^(apps/react(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/react\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||||
echo "rust-auth=$(changed '^(apps/rust-auth(/|$)|infra/docker/rust\.Dockerfile$|\.github/workflows/docker-build-push\.yml$|\.gitmodules$)')" >> "$GITHUB_OUTPUT"
|
echo "rust-auth=$(changed '^(apps/rust-auth(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/rust-auth\.Dockerfile$|\.gitmodules$')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "llm-api=$(changed '^(apps/llm-api(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/llm-api\.Dockerfile$|\.gitmodules$')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Parse repository_dispatch payload
|
||||||
|
|
||||||
- name: Parse repository_dispatch payload
|
- name: Parse repository_dispatch payload
|
||||||
id: dispatch
|
id: dispatch
|
||||||
@@ -87,7 +91,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
case "$SERVICE" in
|
case "$SERVICE" in
|
||||||
scraper-api|elysia-api|react-web|rust-auth) ;;
|
scraper-api|elysia-api|react-web|rust-auth|llm-api) ;;
|
||||||
*)
|
*)
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
@@ -95,7 +99,7 @@ jobs:
|
|||||||
|
|
||||||
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi
|
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi
|
||||||
|
|
||||||
SERVICES=(scraper-api elysia-api react-web rust-auth)
|
SERVICES=(scraper-api elysia-api react-web rust-auth llm-api)
|
||||||
for svc in "${SERVICES[@]}"; do
|
for svc in "${SERVICES[@]}"; do
|
||||||
if [ "$SERVICE" = "$svc" ]; then
|
if [ "$SERVICE" = "$svc" ]; then
|
||||||
echo "${svc}=true" >> "$GITHUB_OUTPUT"
|
echo "${svc}=true" >> "$GITHUB_OUTPUT"
|
||||||
@@ -116,6 +120,7 @@ jobs:
|
|||||||
if [ "${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "elysia-api" "docker-elysia" "apps/elysia"; fi
|
if [ "${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "elysia-api" "docker-elysia" "apps/elysia"; fi
|
||||||
if [ "${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "react-web" "docker-react" "apps/react"; fi
|
if [ "${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "react-web" "docker-react" "apps/react"; fi
|
||||||
if [ "${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "rust-auth" "docker-rust-auth" "apps/rust-auth"; fi
|
if [ "${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "rust-auth" "docker-rust-auth" "apps/rust-auth"; fi
|
||||||
|
if [ "${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "llm-api" "docker-llm-api" "apps/llm-api"; fi
|
||||||
|
|
||||||
JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]"
|
JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]"
|
||||||
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
|
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
|
||||||
@@ -138,6 +143,7 @@ jobs:
|
|||||||
"elysia-api") REPO="https://github.com/asepharyana/asepharyana-hub-elysia.git" ;;
|
"elysia-api") REPO="https://github.com/asepharyana/asepharyana-hub-elysia.git" ;;
|
||||||
"react-web") REPO="https://github.com/asepharyana/asepharyana-hub-react.git" ;;
|
"react-web") REPO="https://github.com/asepharyana/asepharyana-hub-react.git" ;;
|
||||||
"rust-auth") REPO="https://github.com/asepharyana/asepharyana-hub-rust-auth.git" ;;
|
"rust-auth") REPO="https://github.com/asepharyana/asepharyana-hub-rust-auth.git" ;;
|
||||||
|
{indent}"llm-api") REPO="https://github.com/asepharyana/asepharyana-hub-llm-api.git" ;;
|
||||||
*)
|
*)
|
||||||
echo "::error::Unsupported service '$SERVICE'"
|
echo "::error::Unsupported service '$SERVICE'"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -222,6 +228,7 @@ jobs:
|
|||||||
"elysia-api") echo "dockerfile=infra/docker/elysia.Dockerfile" >> $GITHUB_OUTPUT ;;
|
"elysia-api") echo "dockerfile=infra/docker/elysia.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||||
"react-web") echo "dockerfile=infra/docker/react.Dockerfile" >> $GITHUB_OUTPUT ;;
|
"react-web") echo "dockerfile=infra/docker/react.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||||
"rust-auth") echo "dockerfile=infra/docker/rust.Dockerfile" >> $GITHUB_OUTPUT ;;
|
"rust-auth") echo "dockerfile=infra/docker/rust.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||||
|
"llm-api") echo "dockerfile=infra/docker/llm-api.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Build and Push Docker image
|
- name: Build and Push Docker image
|
||||||
@@ -269,12 +276,14 @@ jobs:
|
|||||||
SERVICES["elysia-api"]="elysia.yml"
|
SERVICES["elysia-api"]="elysia.yml"
|
||||||
SERVICES["react-web"]="react.yml"
|
SERVICES["react-web"]="react.yml"
|
||||||
SERVICES["rust-auth"]="rust-auth.yml"
|
SERVICES["rust-auth"]="rust-auth.yml"
|
||||||
|
SERVICES["llm-api"]="llm-api.yml"
|
||||||
|
|
||||||
declare -A PATHS
|
declare -A PATHS
|
||||||
PATHS["scraper-api"]="apps/scraper"
|
PATHS["scraper-api"]="apps/scraper"
|
||||||
PATHS["elysia-api"]="apps/elysia"
|
PATHS["elysia-api"]="apps/elysia"
|
||||||
PATHS["react-web"]="apps/react"
|
PATHS["react-web"]="apps/react"
|
||||||
PATHS["rust-auth"]="apps/rust-auth"
|
PATHS["rust-auth"]="apps/rust-auth"
|
||||||
|
PATHS["llm-api"]="apps/llm-api"
|
||||||
|
|
||||||
# Use git config for possible commits
|
# Use git config for possible commits
|
||||||
git config --local user.email "action@github.com"
|
git config --local user.email "action@github.com"
|
||||||
@@ -286,6 +295,7 @@ jobs:
|
|||||||
if [ "${{ needs.changes.outputs['elysia-api'] }}" == "true" ] && [ "$id" == "elysia-api" ]; then SHOULD_HAVE_RUN=true; fi
|
if [ "${{ needs.changes.outputs['elysia-api'] }}" == "true" ] && [ "$id" == "elysia-api" ]; then SHOULD_HAVE_RUN=true; fi
|
||||||
if [ "${{ needs.changes.outputs['react-web'] }}" == "true" ] && [ "$id" == "react-web" ]; then SHOULD_HAVE_RUN=true; fi
|
if [ "${{ needs.changes.outputs['react-web'] }}" == "true" ] && [ "$id" == "react-web" ]; then SHOULD_HAVE_RUN=true; fi
|
||||||
if [ "${{ needs.changes.outputs['rust-auth'] }}" == "true" ] && [ "$id" == "rust-auth" ]; then SHOULD_HAVE_RUN=true; fi
|
if [ "${{ needs.changes.outputs['rust-auth'] }}" == "true" ] && [ "$id" == "rust-auth" ]; then SHOULD_HAVE_RUN=true; fi
|
||||||
|
if [ "${{ needs.changes.outputs['llm-api'] }}" == "true" ] && [ "$id" == "llm-api" ]; then SHOULD_HAVE_RUN=true; fi
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
llm-api:
|
||||||
|
container_name: llm-api
|
||||||
|
image: ghcr.io/asepharyana/asepharyana-hub/llm-api:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
app-shared-net:
|
||||||
|
aliases:
|
||||||
|
- llm-api
|
||||||
|
volumes:
|
||||||
|
- /root/models/gguf:/root/models/gguf:ro
|
||||||
|
environment:
|
||||||
|
- MODEL_PATH=/root/models/gguf/MiniCPM-V-4.6-Q4_K_M.gguf
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'curl -so /dev/null --connect-timeout 5 http://localhost:8080/health || test $? -eq 22']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
labels:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "8080"
|
||||||
|
prometheus.io/path: "/metrics"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-shared-net:
|
||||||
|
name: app-shared-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# ── Build stage: cargo-chef ──
|
||||||
|
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libclang-dev cmake \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM chef AS planner
|
||||||
|
COPY apps/llm-api .
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
FROM chef AS builder
|
||||||
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/app/target \
|
||||||
|
cargo chef cook --release --recipe-path recipe.json
|
||||||
|
|
||||||
|
COPY apps/llm-api .
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/app/target \
|
||||||
|
cargo build --release && \
|
||||||
|
cp target/release/llm-api /app/llm-api
|
||||||
|
|
||||||
|
# ── Runtime image ──
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
libssl3 \
|
||||||
|
libgomp1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd -g 1001 appgroup && \
|
||||||
|
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/llm-api /app/llm-api
|
||||||
|
|
||||||
|
# Model will be mounted at runtime
|
||||||
|
RUN mkdir -p /root/models/gguf && chown -R appuser:appgroup /root/models/gguf
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["./llm-api"]
|
||||||
@@ -42,6 +42,14 @@ http:
|
|||||||
rule: Host(`auth.asepharyana.my.id`) || Host(`auth.asepharyana.web.id`)
|
rule: Host(`auth.asepharyana.my.id`) || Host(`auth.asepharyana.web.id`)
|
||||||
service: rust-auth-service
|
service: rust-auth-service
|
||||||
tls: {}
|
tls: {}
|
||||||
|
llm-api:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- common-chain@file
|
||||||
|
rule: Host(`ai.asepharyana.my.id`) || Host(`ai.asepharyana.web.id`)
|
||||||
|
service: llm-api-service
|
||||||
|
tls: {}
|
||||||
scraper:
|
scraper:
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
@@ -76,6 +84,10 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: http://rust-auth:3000
|
- url: http://rust-auth:3000
|
||||||
|
llm-api-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://llm-api:8080
|
||||||
scraper-service:
|
scraper-service:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
|
|||||||
Reference in New Issue
Block a user