Compare commits
102
Commits
deploy-llm-api
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59e6c5c6a1 | ||
|
|
4f8a975337 | ||
|
|
c251867f05 | ||
|
|
b06cddca4a | ||
|
|
19aface599 | ||
|
|
528a109d53 | ||
|
|
97b635e8c6 | ||
|
|
99e438d9c5 | ||
|
|
8e850f05c4 | ||
|
|
6c2a4fc9d2 | ||
|
|
3e92f68d78 | ||
|
|
2050129062 | ||
|
|
a6148753ca | ||
|
|
d02989f1a7 | ||
|
|
6e317eb05a | ||
|
|
4e114edb89 | ||
|
|
49123c08b3 | ||
|
|
539b5b8320 | ||
|
|
39df52aab7 | ||
|
|
b326911478 | ||
|
|
8242325ffd | ||
|
|
c075cf282b | ||
|
|
93936efd40 | ||
|
|
5f66cfc12b | ||
|
|
cc8c3f1bf3 | ||
|
|
0c36007da4 | ||
|
|
b7d02647d9 | ||
|
|
ade2798ded | ||
|
|
ef14ca431f | ||
|
|
f681e624b3 | ||
|
|
fdbdcbc1ec | ||
|
|
b7b60e9125 | ||
|
|
722709f6fb | ||
|
|
d20643fbbd | ||
|
|
7da1727603 | ||
|
|
9e9fd229d0 | ||
|
|
40095f75f4 | ||
|
|
3781a8882d | ||
|
|
da2f4a2f2c | ||
|
|
ad22a39a9e | ||
|
|
2f969972a1 | ||
|
|
620f06fbe0 | ||
|
|
e6033c8b12 | ||
|
|
500af023de | ||
|
|
e280d196f9 | ||
|
|
4927338c98 | ||
|
|
54ee903647 | ||
|
|
35cbcdcc92 | ||
|
|
324fe08913 | ||
|
|
e171201b1c | ||
|
|
ec1394bdc5 | ||
|
|
f0787b5f23 | ||
|
|
0df6c35d5a | ||
|
|
5d21848e87 | ||
|
|
b96548bffa | ||
|
|
e19fa76dc3 | ||
|
|
c535ee7858 | ||
|
|
80b5161630 | ||
|
|
1ccc0355ac | ||
|
|
12aed642a9 | ||
|
|
f8e3dccb44 | ||
|
|
1811c6adc2 | ||
|
|
ceeac5c02f | ||
|
|
d0c9f16d5d | ||
|
|
6cbafbfe2e | ||
|
|
d6b9a0806b | ||
|
|
60a1af436f | ||
|
|
2459541677 | ||
|
|
33b999d2b7 | ||
|
|
59f131f951 | ||
|
|
cc8c5088a4 | ||
|
|
d15e8621a9 | ||
|
|
33d5b12ec6 | ||
|
|
b5596e1398 | ||
|
|
46730ec07d | ||
|
|
1ae8a53cd4 | ||
|
|
ff52f8841c | ||
|
|
b747ae5c3b | ||
|
|
64ce812b4e | ||
|
|
9837d9dc50 | ||
|
|
86dda74174 | ||
|
|
d045602f3e | ||
|
|
32049c70ac | ||
|
|
be7348a27d | ||
|
|
43f0df4493 | ||
|
|
f0c7534152 | ||
|
|
a4687c9d5b | ||
|
|
e42e967b7f | ||
|
|
d99bc06df3 | ||
|
|
87df0d7ae2 | ||
|
|
518afa5645 | ||
|
|
2913f515f0 | ||
|
|
05e20df680 | ||
|
|
59e2f9d173 | ||
|
|
f78b826e59 | ||
|
|
d75424634d | ||
|
|
cc8cefb33a | ||
|
|
757b48b446 | ||
|
|
20c81b4018 | ||
|
|
ca81a8b7ee | ||
|
|
ac25a51113 | ||
|
|
10aeda4d4b |
+45
-1
@@ -25,5 +25,49 @@
|
||||
"filePattern": ".claude/skills/deploy-workflow.md",
|
||||
"description": "CI/CD pipeline, Docker patterns, deployment guide"
|
||||
}
|
||||
]
|
||||
],
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "serena-hooks remind --client=claude-code"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__serena__*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "serena-hooks auto-approve --client=claude-code"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "serena-hooks activate --client=claude-code"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "serena-hooks cleanup --client=claude-code"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: deploy-workflow
|
||||
description: Panduan deploy, CI/CD, dan Docker compose patterns untuk Asepharyana Hub
|
||||
description: Panduan deploy, CI/CD, dan Nix/systemd patterns untuk Asepharyana Hub
|
||||
---
|
||||
|
||||
# Deploy & Workflow — Asepharyana Hub
|
||||
|
||||
@@ -14,8 +14,9 @@ asepharyana-hub/
|
||||
├── infra/ # Infrastructure as code
|
||||
│ ├── compose/ # Satu compose file per service
|
||||
│ ├── dapr/ # Dapr component configs
|
||||
│ ├── docker/ # Dockerfiles per service
|
||||
│ └── traefik/ # Static & dynamic Traefik config
|
||||
│ ├── docker/ # Dockerfiles (LEGACY — Docker dihapus)
|
||||
│ ├── traefik/ # Traefik config (LEGACY — diganti Caddy)
|
||||
│ └── caddy/ # Caddyfile.prod (reverse proxy produksi)
|
||||
├── scripts/ # Utility scripts (cleanup, update-deps)
|
||||
└── .github/workflows/ # CI/CD pipelines
|
||||
```
|
||||
@@ -28,9 +29,8 @@ asepharyana-hub/
|
||||
## Infrastructure Patterns
|
||||
|
||||
### Networking
|
||||
- Semua service join **`app-shared-net`** (external Docker bridge)
|
||||
- Service discovery via Docker DNS (container alias)
|
||||
- Traefik sebagai ingress untuk HTTP/S eksternal
|
||||
- Semua service Nix/systemd, inter-service via 127.0.0.1:<port>
|
||||
- Caddy sebagai ingress untuk HTTP/S eksternal (auto-TLS LE, HTTP/3)
|
||||
- Tailscale untuk cross-VPS (PostgreSQL, Redis)
|
||||
|
||||
### Compose File Pattern
|
||||
@@ -78,8 +78,8 @@ networks:
|
||||
- ../../infra/dapr/components:/components
|
||||
```
|
||||
|
||||
### Traefik Routing
|
||||
- Router + service definition di `infra/traefik/dynamic/apps.yaml`
|
||||
### Caddy Routing
|
||||
- Site block di `/etc/caddy/Caddyfile` (ref `infra/caddy/Caddyfile.prod`)
|
||||
- Subdomain pattern: `<service>.asepharyana.my.id` + `<service>.asepharyana.web.id`
|
||||
- TLS cert dari volume mount (bukan auto-acme)
|
||||
|
||||
@@ -92,5 +92,5 @@ networks:
|
||||
1. `shared.yml` (Redis)
|
||||
2. `nats.yml` (NATS message bus)
|
||||
3. `dapr.yml` (Dapr placement)
|
||||
4. `traefik.yml` (Reverse proxy)
|
||||
4. Caddy (reverse proxy)
|
||||
5. Service compose files (apps + Dapr sidecar)
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
name: Deploy Docker to VPS
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build and Push Docker Images']
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'infra/**'
|
||||
- '.github/workflows/deploy-docker.yml'
|
||||
- '.github/workflows/docker-build-push.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevent multiple deployments from running simultaneously
|
||||
concurrency:
|
||||
group: deploy-vps
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: false
|
||||
|
||||
- name: Deploy to VPS
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
VPS_TARGET_DIR: ${{ secrets.VPS_TARGET_DIR }}
|
||||
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Deploy event: ${{ github.event_name }}"
|
||||
echo "Deploy ref: ${{ github.ref }}"
|
||||
echo "Deploy sha: ${{ github.sha }}"
|
||||
|
||||
if [ -z "${SSH_PRIVATE_KEY:-}" ] || [ -z "${VPS_HOST:-}" ] || [ -z "${VPS_USER:-}" ] || [ -z "${VPS_TARGET_DIR:-}" ]; then
|
||||
echo "❌ Deployment secrets are not fully configured. Please set SSH_PRIVATE_KEY, VPS_HOST, VPS_USER, and VPS_TARGET_DIR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H -t ed25519,rsa "$VPS_HOST" >> ~/.ssh/known_hosts
|
||||
|
||||
# Use SSH multiplexing for faster subsequent commands
|
||||
SSH_OPTS=(-o ControlMaster=auto -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlPersist=600 -o StrictHostKeyChecking=yes)
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_TARGET_DIR && mkdir -p $VPS_TARGET_DIR/infra/compose"
|
||||
echo "$ENV_FILE_PRODUCTION" > .env.prod
|
||||
scp "${SSH_OPTS[@]}" .env.prod "$VPS_USER@$VPS_HOST:$VPS_TARGET_DIR/.env"
|
||||
|
||||
echo "🔐 Logging in to GitHub Container Registry..."
|
||||
printf '%s' "$GHCR_TOKEN" | ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "docker login ghcr.io -u '$GHCR_USERNAME' --password-stdin"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "export VPS_TARGET_DIR=$VPS_TARGET_DIR; bash -s" <<'EOF'
|
||||
set -euo pipefail
|
||||
cd "$VPS_TARGET_DIR"
|
||||
|
||||
# Ensure shared network exists
|
||||
docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net
|
||||
|
||||
echo "🔄 Synchronizing repository..."
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "Initializing git repository..."
|
||||
git init
|
||||
git remote add origin https://github.com/asepharyana/asepharyana-hub.git
|
||||
fi
|
||||
git fetch origin main --depth=1 || true
|
||||
|
||||
# Detect changed files before resetting
|
||||
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/hub.yml infra/compose/tools.yml infra/compose/llm-api.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/observability.yml"
|
||||
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
|
||||
|
||||
if git rev-parse HEAD >/dev/null 2>&1; then
|
||||
BEFORE_REV=$(git rev-parse HEAD)
|
||||
git reset --hard FETCH_HEAD
|
||||
AFTER_REV=$(git rev-parse HEAD)
|
||||
|
||||
if [ "$BEFORE_REV" = "$AFTER_REV" ]; then
|
||||
echo "ℹ️ No new commits detected. Using full file list for safety."
|
||||
TARGET_COMPOSE=""
|
||||
else
|
||||
CHANGED=$(git diff --name-only "$BEFORE_REV" "$AFTER_REV" || true)
|
||||
echo "📄 Changed files:"
|
||||
echo "$CHANGED"
|
||||
|
||||
# Detect compose stack changes
|
||||
CHANGED_COMPOSE=$(echo "$CHANGED" | grep '^infra/compose/.*\.yml$' || true)
|
||||
TARGET_COMPOSE=""
|
||||
for f in $CHANGED_COMPOSE; do
|
||||
case " $ALL_COMPOSE_FILES " in
|
||||
*" $f "*) TARGET_COMPOSE="$TARGET_COMPOSE $f" ;;
|
||||
esac
|
||||
done
|
||||
TARGET_COMPOSE=$(printf '%s' "$TARGET_COMPOSE" | xargs || true)
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
echo "🎯 Detected compose stack changes in: $TARGET_COMPOSE"
|
||||
else
|
||||
echo "ℹ️ No stack compose files changed."
|
||||
fi
|
||||
|
||||
# Detect Traefik dynamic config changes
|
||||
CHANGED_TRAEFIK=$(echo "$CHANGED" | grep "^$TRAEFIK_DYNAMIC_DIR/" || true)
|
||||
if [ -n "$CHANGED_TRAEFIK" ]; then
|
||||
echo "🎯 Detected Traefik dynamic config changes:"
|
||||
echo "$CHANGED_TRAEFIK"
|
||||
RELOAD_TRAEFIK="true"
|
||||
else
|
||||
echo "ℹ️ No Traefik dynamic config changes."
|
||||
fi
|
||||
|
||||
# Detect infra file changes (Dockerfiles, config, traefik static)
|
||||
CHANGED_INFRA=$(echo "$CHANGED" | grep '^infra/' | grep -v '^infra/compose/' || true)
|
||||
if [ -n "$CHANGED_INFRA" ]; then
|
||||
echo "📦 Detected other infra file changes:"
|
||||
echo "$CHANGED_INFRA"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
git reset --hard FETCH_HEAD
|
||||
TARGET_COMPOSE=""
|
||||
fi
|
||||
|
||||
if command -v "docker" >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "❌ docker compose is not installed on the remote host."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always include ALL compose files for dependency resolution
|
||||
COMPOSE_ARGS=""
|
||||
for f in $ALL_COMPOSE_FILES; do
|
||||
if [ -f "$f" ]; then
|
||||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
# Extract service names from target compose file(s) for selective up
|
||||
TARGET_SERVICES=""
|
||||
for f in $TARGET_COMPOSE; do
|
||||
if [ -f "$f" ]; then
|
||||
svcs=$($COMPOSE_CMD -f "$f" config --services 2>/dev/null | tr '\n' ' ' | xargs)
|
||||
TARGET_SERVICES="$TARGET_SERVICES $svcs"
|
||||
fi
|
||||
done
|
||||
TARGET_SERVICES=$(echo "$TARGET_SERVICES" | xargs) # trim whitespace
|
||||
echo "🎯 Selective update for services: $TARGET_SERVICES"
|
||||
else
|
||||
echo "🚀 Performing full deployment of all services..."
|
||||
TARGET_SERVICES=""
|
||||
fi
|
||||
|
||||
echo "📥 Pulling images for target services..."
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
PULL_SUCCESS=false
|
||||
# Retry pull up to 3 times to handle transient Docker attestation lease errors
|
||||
for attempt in 1 2 3; do
|
||||
echo "Pull attempt $attempt/3..."
|
||||
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull $TARGET_SERVICES; then
|
||||
echo "✅ Pull succeeded on attempt $attempt"
|
||||
PULL_SUCCESS=true
|
||||
break
|
||||
else
|
||||
echo "⚠️ Pull attempt $attempt failed. Retrying in 5s..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
if [ "$PULL_SUCCESS" != "true" ]; then
|
||||
echo "❌ Failed to pull images after 3 attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧹 Clearing Git locks..."
|
||||
rm -f .git/shallow.lock || true
|
||||
|
||||
echo "🧹 Removing stale target containers by container_name..."
|
||||
# Extract all explicitly defined container_names from compose files and remove them to prevent conflicts
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
for f in $TARGET_COMPOSE; do
|
||||
if [ -f "$f" ]; then
|
||||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
else
|
||||
for f in $ALL_COMPOSE_FILES; do
|
||||
if [ -f "$f" ]; then
|
||||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "🆙 Starting services..."
|
||||
echo "🔍 Debug: Current docker containers:"
|
||||
docker ps -a
|
||||
if [ -n "$TARGET_SERVICES" ]; then
|
||||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d $TARGET_SERVICES
|
||||
else
|
||||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d --remove-orphans
|
||||
fi
|
||||
|
||||
# ── Traefik reload ──
|
||||
if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then
|
||||
echo "🔄 Traefik dynamic config changed — reloading Traefik..."
|
||||
# Traefik watches the dynamic config dir (providers.file.watch=true),
|
||||
# but send SIGHUP as insurance
|
||||
docker kill --signal HUP traefik 2>/dev/null || docker exec traefik kill -HUP 1 2>/dev/null || true
|
||||
echo "✅ Traefik reload signal sent"
|
||||
fi
|
||||
EOF
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/scraper/**'
|
||||
- 'apps/hub/**'
|
||||
- 'apps/tools/**'
|
||||
- 'apps/llm-api/**'
|
||||
- '.github/workflows/docker-build-push.yml'
|
||||
- 'infra/**'
|
||||
- '!infra/compose/**'
|
||||
repository_dispatch:
|
||||
types: [submodule-updated]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME_PREFIX: asepharyana/asepharyana-hub
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────
|
||||
# Phase 1: Detect which services have changed
|
||||
# ──────────────────────────────────────────────
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
scraper-api: ${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
hub: ${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
tools: ${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == '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:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed services
|
||||
id: filter
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
AFTER: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${BEFORE:-}" ] || [[ "$BEFORE" =~ ^0+$ ]]; then
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
else
|
||||
git fetch --no-tags --depth=2 origin "$BEFORE" || true
|
||||
CHANGED_FILES=$(git diff --name-only "$BEFORE" "$AFTER")
|
||||
fi
|
||||
|
||||
changed() {
|
||||
printf '%s\n' "$CHANGED_FILES" | grep -Eq "$1" && echo true || echo false
|
||||
}
|
||||
|
||||
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "hub=$(changed '^(apps/hub(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/hub\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "tools=$(changed '^(apps/tools(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/tools\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "llm-api=$(changed '^(apps/llm-api(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/llm-api\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Parse repository_dispatch payload
|
||||
id: dispatch
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${SERVICE:-}" ]; then
|
||||
echo "::error::repository_dispatch payload missing service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SHA:-}" ]; then
|
||||
echo "::error::repository_dispatch payload missing sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$SERVICE" in
|
||||
scraper-api|hub|tools|llm-api) ;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; fi
|
||||
|
||||
SERVICES=(scraper-api hub tools llm-api)
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if [ "$SERVICE" = "$svc" ]; then
|
||||
echo "${svc}=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "${svc}=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Set matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
SERVICES=()
|
||||
add_service() {
|
||||
SERVICES+=("{\"id\":\"$1\",\"target\":\"$2\",\"path\":\"$3\"}")
|
||||
}
|
||||
|
||||
if [ "${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "scraper-api" "docker-scraper" "apps/scraper"; fi
|
||||
if [ "${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "hub" "docker-hub" "apps/hub"; fi
|
||||
if [ "${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "tools" "docker-tools" "apps/tools"; 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[*]}")]"
|
||||
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
|
||||
|
||||
wait-submodule-ref:
|
||||
needs: [changes]
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Wait for submodule ref
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
case "$SERVICE" in
|
||||
"scraper-api") REPO="https://github.com/asepharyana/asepharyana-hub-scraper.git" ;;
|
||||
"hub") REPO="https://github.com/asepharyana/asepharyana-hub-hub.git" ;;
|
||||
"tools") echo "tools is built from monorepo, no submodule wait needed"; exit 0 ;;
|
||||
"llm-api") REPO="https://github.com/asepharyana/asepharyana-hub-llm-api.git" ;;
|
||||
*)
|
||||
echo "::error::Unsupported service '$SERVICE'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Waiting for $SERVICE commit $SHA in $REPO"
|
||||
TMPDIR=$(mktemp -d)
|
||||
git init "$TMPDIR/probe" >/dev/null
|
||||
git -C "$TMPDIR/probe" remote add origin "$REPO"
|
||||
for attempt in {1..30}; do
|
||||
if git -C "$TMPDIR/probe" fetch --depth=1 origin "$SHA" >/dev/null 2>&1; then
|
||||
echo "Submodule commit $SHA is fetchable for $SERVICE"
|
||||
rm -rf "$TMPDIR"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt/30: $SHA not fetchable yet; waiting 10s"
|
||||
sleep 10
|
||||
done
|
||||
rm -rf "$TMPDIR"
|
||||
|
||||
echo "::error::Submodule commit $SHA for $SERVICE was not fetchable after 300s"
|
||||
exit 1
|
||||
|
||||
# ─────────────────────────────────────────────────
|
||||
# Phase 2: Build and Push Images (Matrix)
|
||||
# ─────────────────────────────────────────────────
|
||||
build:
|
||||
needs: [changes, wait-submodule-ref]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.changes.outputs.matrix) }}
|
||||
if: |
|
||||
always() &&
|
||||
needs.changes.result == 'success' &&
|
||||
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
|
||||
needs.changes.outputs.matrix != '[]'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Sync submodule locally
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
DISPATCH_SHA: ${{ github.event.client_payload.sha }}
|
||||
SUBMODULE_PATH: ${{ matrix.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule update --init --recursive "$SUBMODULE_PATH"
|
||||
if [ "$EVENT_NAME" = "repository_dispatch" ] && [ -n "${DISPATCH_SHA:-}" ]; then
|
||||
cd "$SUBMODULE_PATH"
|
||||
git fetch origin "$DISPATCH_SHA"
|
||||
git checkout "$DISPATCH_SHA"
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
fi
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
run: |
|
||||
SVC_NAME="${{ matrix.id }}"
|
||||
SHORT=${GITHUB_SHA:0:7}
|
||||
echo "image=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "tags=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:sha-${SHORT}" >> $GITHUB_OUTPUT
|
||||
echo "cache-registry=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:buildcache" >> $GITHUB_OUTPUT
|
||||
case "$SVC_NAME" in
|
||||
"scraper-api") echo "dockerfile=infra/docker/scraper.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"hub") echo "dockerfile=infra/docker/hub.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"tools") echo "dockerfile=infra/docker/tools.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"llm-api") echo "dockerfile=infra/docker/llm-api.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
esac
|
||||
|
||||
- name: Build and Push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ${{ steps.meta.outputs.dockerfile }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
COMMIT_COUNT=${{ env.NR_COMMIT_COUNT || github.run_number }}
|
||||
COMMIT_SHA=${{ env.NR_COMMIT_SHA || github.sha }}
|
||||
cache-from: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }}
|
||||
cache-to: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }},mode=max
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Phase 3: Update Manifests and Submodule Refs
|
||||
# ──────────────────────────────────────────────
|
||||
update-manifest:
|
||||
needs: [changes, wait-submodule-ref, build]
|
||||
if: |
|
||||
always() &&
|
||||
needs.changes.result == 'success' &&
|
||||
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
|
||||
- name: Update tags and submodules
|
||||
run: |
|
||||
SHORT_SHA=${GITHUB_SHA:0:7}
|
||||
TAG="sha-$SHORT_SHA"
|
||||
CHANGED=false
|
||||
|
||||
declare -A SERVICES
|
||||
SERVICES["scraper-api"]="scraper.yml"
|
||||
SERVICES["hub"]="hub.yml"
|
||||
SERVICES["tools"]="tools.yml"
|
||||
SERVICES["llm-api"]="llm-api.yml"
|
||||
|
||||
declare -A PATHS
|
||||
PATHS["scraper-api"]="apps/scraper"
|
||||
PATHS["hub"]="apps/hub"
|
||||
PATHS["tools"]="apps/tools"
|
||||
PATHS["llm-api"]="apps/llm-api"
|
||||
|
||||
# Use git config for possible commits
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
for id in "${!SERVICES[@]}"; do
|
||||
SHOULD_HAVE_RUN=false
|
||||
if [ "${{ needs.changes.outputs['scraper-api'] }}" == "true" ] && [ "$id" == "scraper-api" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
if [ "${{ needs.changes.outputs['hub'] }}" == "true" ] && [ "$id" == "hub" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
if [ "${{ needs.changes.outputs['tools'] }}" == "true" ] && [ "$id" == "tools" ]; 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 [ "$SHOULD_HAVE_RUN" == "true" ]; then
|
||||
COMPOSE_FILE="infra/compose/${SERVICES[$id]}"
|
||||
if [ -f "$COMPOSE_FILE" ]; then
|
||||
echo "Updating $COMPOSE_FILE to $TAG"
|
||||
sed -i "s|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:$TAG|g" "$COMPOSE_FILE"
|
||||
git add "$COMPOSE_FILE"
|
||||
CHANGED=true
|
||||
fi
|
||||
|
||||
# If it's a repository_dispatch for this specific service, update its submodule pointer
|
||||
if [ "${{ github.event_name }}" == "repository_dispatch" ] && [ "${{ github.event.client_payload.service }}" == "$id" ]; then
|
||||
SHA_DISPATCH="${{ github.event.client_payload.sha }}"
|
||||
SUB_PATH="${PATHS[$id]}"
|
||||
if [ -n "$SHA_DISPATCH" ]; then
|
||||
echo "Updating submodule $SUB_PATH to $SHA_DISPATCH"
|
||||
git submodule update --init "$SUB_PATH"
|
||||
git -C "$SUB_PATH" fetch origin "$SHA_DISPATCH"
|
||||
git -C "$SUB_PATH" checkout "$SHA_DISPATCH"
|
||||
git add "$SUB_PATH"
|
||||
CHANGED=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CHANGED" == "true" ]; then
|
||||
git commit -m "chore: update manifests and submodules [skip ci]"
|
||||
|
||||
for attempt in {1..3}; do
|
||||
if git pull --rebase origin main && git push origin main; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Manifest push attempt $attempt/3 failed; retrying"
|
||||
git rebase --abort || true
|
||||
git pull --rebase origin main || true
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "::error::Failed to push manifest update after 3 attempts"
|
||||
exit 1
|
||||
else
|
||||
echo "No changes detected."
|
||||
fi
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Publish to FlakeHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
flakehub-publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: DeterminateSystems/determinate-nix-action@main
|
||||
- uses: DeterminateSystems/flakehub-push@main
|
||||
with:
|
||||
visibility: public
|
||||
rolling: true
|
||||
@@ -0,0 +1,103 @@
|
||||
name: Nix Build & Deploy — All Services
|
||||
|
||||
on:
|
||||
# No `paths` filter: GitHub's path filters do not match submodule gitlink
|
||||
# changes, so a submodule pointer update (e.g. from update-submodule.yml)
|
||||
# would never trigger this deploy. Run on every push to main instead.
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: nix-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service: [hub, scraper, tools-gateway, tools-workers, tools-frontend, llm-api]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
with:
|
||||
determinate: false
|
||||
extra-conf: |
|
||||
sandbox = false
|
||||
accept-flake-config = true
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v14
|
||||
with:
|
||||
use-flakehub: false
|
||||
|
||||
- name: Build ${{ matrix.service }}
|
||||
id: build
|
||||
run: |
|
||||
nix build .#${{ matrix.service }} --impure --option sandbox false --print-build-logs
|
||||
STORE_PATH=$(readlink result)
|
||||
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ ${{ matrix.service }}: $STORE_PATH"
|
||||
|
||||
- name: Setup SSH key
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; }
|
||||
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy ${{ matrix.service }} to VPS
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
STORE_PATH="${{ steps.build.outputs.store-path }}"
|
||||
echo "=== Copying ${{ matrix.service }}: $STORE_PATH ==="
|
||||
nix copy --to "ssh://$VPS_USER@$VPS_HOST" "$STORE_PATH"
|
||||
|
||||
echo "=== Updating profile ==="
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-env --profile /nix/var/nix/profiles/${{ matrix.service }} --set '$STORE_PATH'"
|
||||
|
||||
echo "=== Restarting service ==="
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo systemctl restart ${{ matrix.service }}" || echo " ⚠️ restart failed (may not be enabled yet)"
|
||||
|
||||
echo "✅ ${{ matrix.service }} deployed"
|
||||
|
||||
cleanup:
|
||||
# Bersihkan sampah Nix di VPS SETELAH deploy: hapus generasi profile lama
|
||||
# + nix store gc. Profil yang sedang dipakai tidak disentuh.
|
||||
needs: build-and-deploy
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Nix GC on VPS
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo /usr/local/bin/nix-gc-vps.sh" || echo "⚠️ Nix GC gagal (non-fatal)"
|
||||
@@ -11,6 +11,7 @@ node_modules
|
||||
.turbo/
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.serena/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
@@ -63,3 +64,4 @@ docs/todo.md
|
||||
**/vendor/
|
||||
|
||||
.codegraph/
|
||||
result
|
||||
|
||||
+52
-42
@@ -10,12 +10,12 @@ asepharyana-hub/
|
||||
│ ├── adr/ # Architecture Decision Records
|
||||
│ ├── add-new-app.md # Guide for adding new services
|
||||
│ └── superpowers/ # Project capabilities tracking
|
||||
├── infra/ # Infrastructure as code
|
||||
│ ├── compose/ # Docker Compose files per service
|
||||
├── infra/ # Infrastructure as code (LEGACY Docker layout)
|
||||
│ ├── compose/ # Docker Compose files (LEGACY — Docker dihapus 2026-08-02)
|
||||
│ ├── config/ # Infrastructure configuration
|
||||
│ ├── docker/ # Dockerfiles per service
|
||||
│ └── traefik/ # Traefik reverse proxy config
|
||||
│ └── dynamic/ # Dynamic routing rules (YAML)
|
||||
│ ├── docker/ # Dockerfiles (LEGACY)
|
||||
│ ├── traefik/ # Traefik config (LEGACY — diganti Caddy)
|
||||
│ └── caddy/ # Caddyfile.prod (reverse proxy produksi)
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── git-hooks/ # Git hook scripts
|
||||
│ ├── cleanup-ghcr.sh # GHCR image cleanup
|
||||
@@ -38,9 +38,10 @@ asepharyana-hub/
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------- |
|
||||
| Reverse Proxy | Traefik v3.6 | TLS termination, routing, middleware (rate-limit, headers, auth) |
|
||||
| Container Runtime | Docker + Docker Compose | Service isolation and orchestration |
|
||||
| Container Registry | GHCR (ghcr.io) | Docker image storage |
|
||||
| Reverse Proxy | Caddy 2.11.4 | TLS termination (auto-LE), routing, HTTP/3, keep-alive tuning |
|
||||
| Runtime | Nix + systemd | Service isolation and orchestration (Docker dihapus 2026-08-02) |
|
||||
| Deployment | GitHub Actions | nix build → nix copy ssh:// → systemctl restart |
|
||||
| Secrets | Bitwarden Secrets Manager (BWS) | Central secret store, bws-exec wrapper |
|
||||
| Networking | Tailscale | Secure overlay network between VPS nodes |
|
||||
| Message Bus | NATS + JetStream | Event-driven pub/sub, job queues, streaming |
|
||||
| Runtime Sidecar | Dapr | Service invocation, pub/sub abstraction, state management |
|
||||
@@ -49,41 +50,50 @@ asepharyana-hub/
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Traefik Reverse Proxy
|
||||
### Caddy Reverse Proxy
|
||||
|
||||
Traefik runs as the entry point for all HTTP/S traffic. It is configured via:
|
||||
Caddy 2.11.4 runs as the entry point for all HTTP/S traffic (systemd `caddy.service`, `/etc/caddy/Caddyfile`). It is configured via:
|
||||
|
||||
- **Static config**: CLI arguments in `infra/compose/traefik.yml` — entry points, providers, plugins
|
||||
- **Dynamic config**: `infra/traefik/dynamic/` — routers, services, middlewares, TLS
|
||||
- **Docker provider**: Auto-discovers containers with `traefik.enable=true` labels
|
||||
- **File provider**: Loads `apps.yaml` (routers/services), `middlewares.yaml`, `ssl.yaml`
|
||||
- **Auto-TLS**: Let's Encrypt per-domain (email asepharyana@gmail.com)
|
||||
- **HTTP/3**: h3 enabled on :443 (QUIC)
|
||||
- **Snippet `(proxy)`**: shared handler — `encode zstd gzip`, security headers, keep-alive upstream (keepalive 120s, max_conns_per_host 100, dial_timeout 3s)
|
||||
- **Upload domain** (`upload.asepharyana.my.id`): `flush_interval -1` (streaming), `request_body max_size 0` (unlimited)
|
||||
|
||||
Key middleware chains (`infra/traefik/dynamic/middlewares.yaml`):
|
||||
Reference: `infra/caddy/Caddyfile.prod`. Legacy Traefik configs stay under `infra/traefik/` for reference only.
|
||||
|
||||
- `secure-headers` — SSL redirect, HSTS, XSS protection, CSP
|
||||
- `compress` — Gzip compression for responses over 256 bytes
|
||||
- `rate-limit` — 100 avg / 50 burst requests
|
||||
- `buffer` — 10MB request/response body limit
|
||||
- `block-sensitive-paths` — blocks `.env`, `.git`, `/wp-admin` etc.
|
||||
- `common-chain` — composes secure-headers + compress + retry + rate-limit + buffer
|
||||
### Port Mapping (Produksi)
|
||||
|
||||
All services route through Traefik on port 443 (TLS), with automatic HTTP-to-HTTPS redirect.
|
||||
| Service | Port | Domain |
|
||||
|---------|------|--------|
|
||||
| TeleUploader | 4000 | upload.asepharyana.my.id |
|
||||
| GMW backend | 4001 | (internal) |
|
||||
| pr-agent | 4002 | pr-agent.asepharyana.my.id |
|
||||
| hub frontend | 4003 | asepharyana.my.id |
|
||||
| lidm frontend | 4004 | lidm.asepharyana.my.id |
|
||||
| lidm backend | 4005 | lidm-api.asepharyana.my.id |
|
||||
| zeavis API | 4006 | api-zeavisedu.asepharyana.my.id |
|
||||
| tools frontend | 4007 | tools.asepharyana.my.id |
|
||||
| tools gateway | 4008 | (internal) |
|
||||
| GMW proxy | 4009 | imphnen.asepharyana.my.id |
|
||||
| llm-api | 4010 | ai.asepharyana.my.id |
|
||||
| zeavisedu nginx | 4011 | zeavisedu.asepharyana.my.id |
|
||||
| zeavis ML | 4012 | ml-zeavisedu.asepharyana.my.id |
|
||||
| dashboard | 4013 | dashboard.asepharyana.my.id |
|
||||
| 9router | 4014 | 9router.asepharyana.my.id |
|
||||
| scraper | 4091 | scraper.asepharyana.my.id |
|
||||
|
||||
### Docker Compose
|
||||
### Nix + systemd Deployment
|
||||
|
||||
Each service has its own Compose file under `infra/compose/`. All services join the `app-shared-net` external Docker network, enabling inter-service communication by container name.
|
||||
|
||||
Shared services:
|
||||
|
||||
- `infra/compose/shared.yml` — Redis (alias: `redis`)
|
||||
- `infra/compose/traefik.yml` — Traefik reverse proxy
|
||||
|
||||
Service compose files are combined during deployment:
|
||||
Docker dihapus dari produksi (2026-08-02). Semua service deploy via Nix flakes + systemd:
|
||||
|
||||
```bash
|
||||
docker compose -f traefik.yml -f shared.yml -f scraper.yml up -d
|
||||
nix build .#default --impure --option sandbox false
|
||||
nix copy --to ssh://vps /nix/store/<hash>
|
||||
systemctl restart <service>
|
||||
```
|
||||
|
||||
CI/CD: GitHub Actions (`deploy.yml`) → nix build → nix copy → systemctl restart. Flake dibatasi `x86_64-linux` (nixpkgs 26.11 drop darwin).
|
||||
|
||||
### Tailscale Networking
|
||||
|
||||
```mermaid
|
||||
@@ -99,12 +109,12 @@ graph TB
|
||||
REDIS[Redis]
|
||||
end
|
||||
|
||||
subgraph "orangevps Containers"
|
||||
TRAEFIK[Traefik :443]
|
||||
subgraph "orangevps Services (Nix)"
|
||||
CADDY[Caddy :443]
|
||||
SCRAPER[scraper-api :4091]
|
||||
end
|
||||
|
||||
TRAEFIK --> SCRAPER
|
||||
CADDY --> SCRAPER
|
||||
|
||||
style IMRNES fill:#3a7,color:#fff
|
||||
style ORANGEVPS fill:#37a,color:#fff
|
||||
@@ -127,17 +137,17 @@ This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orange
|
||||
sequenceDiagram
|
||||
participant User as Browser/Client
|
||||
participant DNS as Cloudflare DNS
|
||||
participant Traefik as Traefik Proxy
|
||||
participant Caddy as Caddy Proxy
|
||||
participant App as Application Container
|
||||
participant DB as PostgreSQL (imrnes via Tailscale)
|
||||
participant Redis as Redis (imrnes via Tailscale)
|
||||
|
||||
User->>DNS: asepharyana.my.id
|
||||
DNS->>User: A/AAAA record → orangevps VPS IP
|
||||
User->>Traefik: HTTPS request :443
|
||||
Traefik->>Traefik: TLS termination
|
||||
Traefik->>Traefik: Middleware chain (headers, rate-limit, buffer)
|
||||
Traefik->>App: HTTP reverse-proxy (internal network)
|
||||
User->>Caddy: HTTPS request :443
|
||||
Caddy->>Caddy: TLS termination
|
||||
Caddy->>Caddy: encode + headers
|
||||
Caddy->>App: HTTP reverse-proxy (127.0.0.1:<port>)
|
||||
|
||||
alt Database query
|
||||
App->>DB: sqlx/Drizzle query via Tailscale
|
||||
@@ -147,8 +157,8 @@ sequenceDiagram
|
||||
Cache-->>App: Cached value
|
||||
end
|
||||
|
||||
App-->>Traefik: HTTP response
|
||||
Traefik-->>User: HTTPS response
|
||||
App-->>Caddy: HTTP response
|
||||
Caddy-->>User: HTTPS response
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2026-08-02]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Infra overhaul**: Docker + Traefik dihapus dari produksi → Caddy 2.11.4 (reverse proxy, auto-TLS LE, HTTP/3) + Nix/systemd services.
|
||||
- **Port migration**: semua service pindah ke port 4000-an (hub 4003, tools 4007/4008, scraper 4091, llm-api 4010, dll).
|
||||
- **DB via PgBouncer pool**: semua service konek ke imrnes 100.121.180.82:6432 (bukan :5432 langsung).
|
||||
- **Secrets**: Bitwarden Secrets Manager (BWS) sebagai central secret store, wrapper bws-exec.
|
||||
- **Flake**: dibatasi x86_64-linux (nixpkgs 26.11 drop darwin).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Repository Overview
|
||||
|
||||
Asepharyana Hub is a **hub monorepo** for Asep Haryana Saputra's portfolio ecosystem. Application services live in separate repos imported as Git submodules under `apps/`. Infrastructure (Docker Compose, Traefik, Dapr) lives in `infra/`.
|
||||
Asepharyana Hub is a **hub monorepo** for Asep Haryana Saputra's portfolio ecosystem. Application services live in separate repos imported as Git submodules under `apps/`. Production infrastructure: Caddy reverse proxy + Nix/systemd services (Docker/Traefik removed 2026-08-02; legacy configs under `infra/` marked LEGACY).
|
||||
|
||||
```
|
||||
asepharyana-hub/
|
||||
@@ -13,9 +13,9 @@ asepharyana-hub/
|
||||
│ └── scraper/ # Rust scraper API (asepharyana-hub-scraper)
|
||||
├── docs/ # ADRs, deployment guide, new-app guide
|
||||
├── infra/
|
||||
│ ├── compose/ # One Docker Compose file per service
|
||||
│ ├── compose/ # Docker Compose files (LEGACY — Docker dihapus)
|
||||
│ ├── dapr/ # Dapr config + component definitions
|
||||
│ ├── docker/ # Dockerfiles per service
|
||||
│ ├── docker/ # Dockerfiles (LEGACY)
|
||||
│ └── traefik/ # Reverse proxy config (static + dynamic)
|
||||
├── scripts/ # Utility scripts (cleanup, update-deps, git hooks)
|
||||
└── .github/workflows/ # CI/CD pipelines
|
||||
@@ -31,23 +31,22 @@ asepharyana-hub/
|
||||
- `apps/tools` → `asepharyana/asepharyana-hub-tools`.
|
||||
|
||||
### Infrastructure Stack
|
||||
- **Traefik v3.6** — reverse proxy, TLS termination, middleware chain, Prometheus metrics (`--metrics.prometheus=true`)
|
||||
- **Caddy 2.11.4** — reverse proxy, TLS termination (auto-LE), HTTP/3, zstd/gzip, keep-alive tuning (`/etc/caddy/Caddyfile`, ref `infra/caddy/Caddyfile.prod`)
|
||||
- **NATS + JetStream** — message broker with persistent streaming
|
||||
- **Dapr** — sidecar runtime (pub/sub abstraction, state management, service invocation)
|
||||
- **Redis (Alpine)** — cache, session store, Dapr state store & pub/sub backend
|
||||
- **Prometheus** — metrics backend with Docker service discovery (`docker_sd_configs`). Auto-discovers containers with `prometheus.io/scrape=true` label.
|
||||
- **Prometheus** — metrics backend with `file_sd_configs` target files.
|
||||
- **Jaeger** — distributed tracing backend (all-in-one), OTLP receiver
|
||||
- **Tailscale** — secure overlay network between VPS nodes (PostgreSQL on `imrnes`, containers on `orangevps`)
|
||||
|
||||
### Monitoring
|
||||
- **Hub dashboard** at `/dashboard` (Next.js client page, auto-refresh 15s)
|
||||
- **Dashboard API** at `/api/dashboard` — returns JSON with Docker containers, Jaeger traces, Prometheus metrics (RPS, latency, errors, node CPU/RAM/Disk)
|
||||
- **Docker socket** mounted on `hub` container (`--group-add 988`) for container discovery
|
||||
- **Prometheus** auto-scrapes Traefik for per-service request metrics
|
||||
- **Dashboard API** at `/api/dashboard` — returns JSON with systemd services, Jaeger traces, Prometheus metrics (RPS, latency, errors, node CPU/RAM/Disk)
|
||||
- **Prometheus** scrapes node-exporter + app metrics endpoints
|
||||
|
||||
### Networking
|
||||
- All containers join `app-shared-net` (external Docker bridge network). Service discovery via Docker DNS (container name aliases).
|
||||
- Traefik handles all external HTTP/S traffic on port 443.
|
||||
- All services run as Nix/systemd units; inter-service via 127.0.0.1:<port>.
|
||||
- Caddy handles all external HTTP/S traffic on port 443 (and HTTP/3 UDP).
|
||||
- Cross-VPS traffic (DB, Redis) goes through Tailscale (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a route in the main routing table (managed by `tailscale-routes.service`).
|
||||
|
||||
## Commands
|
||||
@@ -62,7 +61,7 @@ bun run ci # Biome CI mode (no writes, exit code on issues)
|
||||
bun run format # Format only
|
||||
bun run lint # Lint only
|
||||
|
||||
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest . # Build image
|
||||
# Nix build (produksi): nix build .#default --impure --option sandbox false
|
||||
```
|
||||
|
||||
### Validate YAML
|
||||
@@ -76,8 +75,8 @@ for f in infra/compose/*.yml; do docker compose -f "$f" config >/dev/null && ech
|
||||
| Workflow | Trigger | Action |
|
||||
|----------|---------|--------|
|
||||
| `lint.yml` | PR/push to main touching `*.json`, `*.js`, `biome.json` | `bun run ci` (Biome lint) |
|
||||
| `docker-build-push.yml` | Push to main touching `apps/**`/`infra/**`, or `repository_dispatch` | Build Docker images per changed service, push to GHCR, update compose manifests |
|
||||
| `deploy-docker.yml` | After build completes, or push touching `infra/**` | SSH to VPS (orangevps), pull images, restart containers selectively |
|
||||
| `deploy.yml` | Push to main | nix build → nix copy ssh:// → systemctl restart |
|
||||
| `docker-build-push.yml` | LEGACY (Docker dihapus) | LEGACY |
|
||||
| `security.yml` | PR to main + weekly Monday | CodeQL analysis (Rust) |
|
||||
| `update-submodule.yml` | `repository_dispatch` | Update submodule pointer in hub repo |
|
||||
|
||||
@@ -99,11 +98,11 @@ Each service gets one compose file. Containers join `app-shared-net` with a `con
|
||||
### Dapr Sidecar Pattern
|
||||
Each app gets a companion `daprd` sidecar container. Dapr components (pubsub, statestore) are mounted from `infra/dapr/components/`. The sidecar communicates with NATS for pub/sub and Dapr placement for actor coordination.
|
||||
|
||||
### Traefik Routing
|
||||
- Routers + services defined in `infra/traefik/dynamic/apps.yaml`
|
||||
### Caddy Routing
|
||||
- Site blocks in `/etc/caddy/Caddyfile` (ref `infra/caddy/Caddyfile.prod`)
|
||||
- Subdomain pattern: `<service>.asepharyana.my.id` and `<service>.asepharya.web.id`
|
||||
- TLS certs from volume mounts (not auto-ACME)
|
||||
- Middleware chain: `secure-headers` → `compress` → `retry` → `rate-limit` → `buffer`
|
||||
- Auto-TLS via Let's Encrypt
|
||||
- Shared handler snippet `(proxy)`: `encode zstd gzip` + security headers + keep-alive tuning
|
||||
|
||||
### Image Tagging
|
||||
- `sha-<short-sha>` — immutable, for deterministic rollbacks
|
||||
@@ -115,9 +114,9 @@ Each app gets a companion `daprd` sidecar container. Dapr components (pubsub, st
|
||||
|
||||
1. Create a separate repo for the app code
|
||||
2. Add as submodule: `git submodule add <url> apps/<name>`
|
||||
3. Create Dockerfile in `infra/docker/`
|
||||
3. Create Nix flake package + systemd unit
|
||||
4. Create compose file in `infra/compose/` (app + Dapr sidecar)
|
||||
5. Add Traefik router in `infra/traefik/dynamic/apps.yaml`
|
||||
5. Add Caddy site block in `/etc/caddy/Caddyfile`
|
||||
6. Add build job in `.github/workflows/docker-build-push.yml`
|
||||
7. See `docs/add-new-app.md` for full guide
|
||||
|
||||
|
||||
@@ -1,181 +1,232 @@
|
||||
# Asepharyana Hub
|
||||
# Architecture
|
||||
|
||||
Hub repo untuk ekosistem portfolio dan layanan pendukung milik Asep Haryana Saputra.
|
||||
Aplikasi dipisah sebagai submodule agar frontend, API, dan service pendukung bisa dikembangkan serta di-deploy secara independen.
|
||||
## Hub Repository Structure Overview
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Path | Notes |
|
||||
| :------ | :------------- | :----------------------------- |
|
||||
| Scraper | `apps/scraper` | Web scraper service + Dapr SDK |
|
||||
| NATS | — | Message broker + JetStream |
|
||||
| Dapr | — | Sidecar runtime (per service) |
|
||||
|
||||
## Infrastructure
|
||||
|
||||
File compose berada di `infra/compose/`:
|
||||
|
||||
- `traefik.yml`: reverse proxy Traefik untuk semua layanan.
|
||||
- `shared.yml`: Redis (cache + Dapr state store).
|
||||
- `nats.yml`: NATS message broker dengan JetStream persistence.
|
||||
- `dapr.yml`: Dapr placement service untuk koordinasi sidecar.
|
||||
- `scraper.yml`: manifest deploy per service (app + Dapr sidecar).
|
||||
|
||||
Dockerfile per service berada di `infra/docker/`.
|
||||
|
||||
## Docker Image Builds
|
||||
|
||||
Build image via Dockerfile:
|
||||
|
||||
```bash
|
||||
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest .
|
||||
```diff
|
||||
asepharyana-hub/
|
||||
├── apps/ # Application services (Git submodules)
|
||||
│ └── scraper/ # Web scraper service
|
||||
├── docs/ # Documentation
|
||||
│ ├── adr/ # Architecture Decision Records
|
||||
│ ├── add-new-app.md # Guide for adding new services
|
||||
│ └── superpowers/ # Project capabilities tracking
|
||||
├── infra/ # Infrastructure as code
|
||||
│ ├── compose/ # Docker Compose files per service
|
||||
│ ├── config/ # Infrastructure configuration
|
||||
│ ├── docker/ # Dockerfiles per service
|
||||
│ └── traefik/ # Traefik reverse proxy config
|
||||
│ └── dynamic/ # Dynamic routing rules (YAML)
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── git-hooks/ # Git hook scripts
|
||||
│ ├── cleanup-ghcr.sh # GHCR image cleanup
|
||||
│ └── update-deps.sh # Dependency update helper
|
||||
├── .github/workflows/ # CI/CD pipelines
|
||||
├── eslint.config.mjs # Root ESLint config
|
||||
├── package.json # Root formatting/lint helper scripts
|
||||
└── .prettierrc # Prettier formatting rules
|
||||
```
|
||||
|
||||
Tag and push:
|
||||
## Technology Stack
|
||||
|
||||
### Services
|
||||
|
||||
|| Service | Path | Language/Runtime | Framework | Database | Key Libraries |
|
||||
||---------|----------------|------------------|-----------|----------|---------------|
|
||||
|| **scraper** | `apps/scraper` | — | — | — | — |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
|| Component | Technology | Purpose |
|
||||
||---------------------|-------------------------|------------------------------------------------------------------|
|
||||
|| Reverse Proxy | Traefik v3.6 | TLS termination, routing, middleware (rate-limit, headers, auth) |
|
||||
|| Container Runtime | Docker + Docker Compose | Service isolation and orchestration |
|
||||
|| Container Registry | GHCR (ghcr.io) | Docker image storage |
|
||||
|| Networking | Tailscale | Secure overlay network between VPS nodes |
|
||||
|| Message Bus | NATS + JetStream | Event-driven pub/sub, job queues, streaming |
|
||||
|| Runtime Sidecar | Dapr | Service invocation, pub/sub abstraction, state management |
|
||||
|| Cache & State | Redis (Alpine) | Session store, rate limit counters, caching, Dapr state store |
|
||||
|| CI/CD | GitHub Actions | Build, test, deploy automation |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
### Traefik Reverse Proxy
|
||||
|
||||
Traefik runs as the entry point for all HTTP/S traffic. It is configured via:
|
||||
|
||||
- **Static config**: CLI arguments in `infra/compose/traefik.yml` — entry points, providers, plugins
|
||||
- **Dynamic config**: `infra/traefik/dynamic/` — routers, services, middlewares, TLS
|
||||
- **Docker provider**: Auto-discovers containers with `traefik.enable=true` labels
|
||||
- **File provider**: Loads `apps.yaml` (routers/services), `middlewares.yaml`, `ssl.yaml`
|
||||
|
||||
Key middleware chains (`infra/traefik/dynamic/middlewares.yaml`):
|
||||
|
||||
- `secure-headers` — SSL redirect, HSTS, XSS protection, CSP
|
||||
- `compress` — Gzip compression for responses over 256 bytes
|
||||
- `rate-limit` — 100 avg / 50 burst requests
|
||||
- `buffer` — 10MB request/response body limit
|
||||
- `block-sensitive-paths` — blocks `.env`, `.git`, `/wp-admin` etc.
|
||||
- `common-chain` — composes secure-headers + compress + retry + rate-limit + buffer
|
||||
|
||||
All services route through Traefik on port 443 (TLS), with automatic HTTP-to-HTTPS redirect.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Each service has its own Compose file under `infra/compose/`. All services join the `app-shared-net` external Docker network, enabling inter-service communication by container name.
|
||||
|
||||
Shared services:
|
||||
|
||||
- `infra/compose/shared.yml` — Redis (alias: `redis`)
|
||||
- `infra/compose/traefik.yml` — Traefik reverse proxy
|
||||
|
||||
Service compose files are combined during deployment:
|
||||
|
||||
```bash
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
|
||||
docker tag scraper-api:latest ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-$SHORT_SHA
|
||||
docker push ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-$SHORT_SHA
|
||||
docker compose -f traefik.yml -f shared.yml -f scraper.yml up -d
|
||||
```
|
||||
|
||||
## Local Development
|
||||
### Tailscale Networking
|
||||
|
||||
### 1) Jalankan dependency bersama
|
||||
### Arsitektur
|
||||
|
||||
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/shared.yml up -d
|
||||
ip route add 100.64.0.0/10 dev tailscale0 table main
|
||||
```
|
||||
|
||||
### 2) Jalankan service yang dibutuhkan
|
||||
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orangevps` VPS.
|
||||
|
||||
Refer to each service's own documentation for development setup.
|
||||
### Data Flow
|
||||
|
||||
## API Docs and Monitoring
|
||||
### Request Flow (Production)
|
||||
|
||||
Refer to each service's own documentation for API docs.
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Browser/Client
|
||||
participant DNS as Cloudflare DNS
|
||||
participant Traefik as Traefik Proxy
|
||||
participant App as Application Container
|
||||
participant DB as PostgreSQL (imrnes via Tailscale)
|
||||
participant Redis as Redis (imrnes via Tailscale)
|
||||
|
||||
User->>DNS: asepharyana.my.id
|
||||
DNS->>User: A/AAAA record → orangevps VPS IP
|
||||
User->>Traefik: HTTPS request :443
|
||||
Traefik->>Traefik: TLS termination
|
||||
Traefik->>Traefik: Middleware chain (headers, rate-limit, buffer)
|
||||
Traefik->>App: HTTP reverse-proxy (internal network)
|
||||
|
||||
alt Database query
|
||||
App->>DB: sqlx/Drizzle query via Tailscale
|
||||
DB-->>App: Result set
|
||||
else Cache lookup
|
||||
App->>Cache: GET/SET via Tailscale
|
||||
Cache-->>App: Cached value
|
||||
end
|
||||
|
||||
App-->>Traefik: HTTP response
|
||||
Traefik-->>User: HTTPS response
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Push to main] --> B{Changed paths?}
|
||||
B -->|apps/** or infra/docker/**| C[Build Docker Images]
|
||||
B -->|infra/compose/**| D[Deploy to VPS]
|
||||
B -->|apps/*/src/**/*.ts| E[Lint + TypeCheck]
|
||||
|
||||
C --> F[Push to GHCR]
|
||||
F --> G[Update Compose tags]
|
||||
G --> D
|
||||
|
||||
D --> H[SSH into VPS]
|
||||
H --> I[Pull images]
|
||||
I --> J[docker compose up -d]
|
||||
|
||||
subgraph "Build Phase"
|
||||
C
|
||||
F
|
||||
G
|
||||
end
|
||||
|
||||
subgraph "Deploy Phase"
|
||||
D
|
||||
H
|
||||
I
|
||||
J
|
||||
end
|
||||
```
|
||||
|
||||
### Deployment Architecture
|
||||
|
||||
### Image Tags
|
||||
|
||||
- `latest` — mutable, for convenience
|
||||
- `sha-<short-sha>` — immutable, for deterministic rollbacks
|
||||
- Build cache: `sha-<short>-buildcache`
|
||||
|
||||
Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- Pipeline memakai image tag berbasis commit SHA (`sha-<short-sha>`), bukan `latest`.
|
||||
- Deploy Compose sekarang mencakup `infra/compose/*.yml` dan `deploy-docker.yml` akan berjalan langsung ketika `infra/compose/**` berubah.
|
||||
- Selective deployment: hanya compose file yg berubah yang di-redeploy.
|
||||
|
||||
## Networking & Tailscale
|
||||
|
||||
### Arsitektur
|
||||
|
||||
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`).
|
||||
|
||||
| VPS | Tailscale IP | Service |
|
||||
| :--------- | :-------------- | :------------------------------------- |
|
||||
| `imrnes` | `100.121.180.82` | PostgreSQL, Redis |
|
||||
| `orangevps` | `100.79.111.61` | App containers (Traefik, scraper-api) |
|
||||
| `archlinux` | `100.84.39.83` | _(development machine)_ |
|
||||
|
||||
### Container → Tailscale Connectivity
|
||||
|
||||
Docker containers di bridge network (`app-shared-net`) **tidak otomatis bisa access Tailscale IPs** karena Tailscale menggunakan **custom policy routing** (routes di `table 52`, bukan `main` table).
|
||||
|
||||
#### Fix: Tailscale Route di Main Table
|
||||
|
||||
Agar container bisa reach Tailscale IPs (untuk DB, Redis, dll), tambahkan route ke `main` routing table:
|
||||
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
|
||||
|
||||
```bash
|
||||
# Manual (hilang setelah reboot)
|
||||
ip route add 100.64.0.0/10 dev tailscale0 table main
|
||||
|
||||
# Persistent (systemd service)
|
||||
# Sudah dikonfigurasi sebagai /etc/systemd/system/tailscale-routes.service
|
||||
# Service ini berjalan otomatis setelah tailscaled start
|
||||
systemctl enable tailscale-routes.service
|
||||
systemctl start tailscale-routes.service
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orangevps` VPS.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Service yang connect ke Tailscale IP:
|
||||
|
||||
```env
|
||||
# PostgreSQL di imrnes
|
||||
DATABASE_URL=postgres://user:pass@100.121.180.82:5432/dbname
|
||||
DATABASE_URL=postgres://user:***@100.121.180.82:6432/dbname
|
||||
|
||||
# Redis di imrnes
|
||||
REDIS_URL=redis://100.121.180.82:6379
|
||||
```
|
||||
|
||||
#### Persistent Systemd Service
|
||||
## Submodule Strategy
|
||||
|
||||
File: `/etc/systemd/system/tailscale-routes.service`
|
||||
Each application lives in its own Git repository and is imported as a submodule into `apps/`. This approach:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Add Tailscale routes to main routing table
|
||||
After=tailscaled.service
|
||||
Requires=tailscaled.service
|
||||
- **Enables independent development** — each service can be developed, tested, and versioned separately
|
||||
- **Pins exact commits** — the super-repository tracks exact submodule SHAs, enabling reproducible deployments
|
||||
- **Supports `repository_dispatch`** — when a submodule receives a push, it can trigger the super-repository to build and deploy only that service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
|
||||
RemainAfterExit=yes
|
||||
### Submodule Lifecycle
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
1. Developer pushes to a submodule (e.g., `apps/scraper`)
|
||||
2. Submodule's GitHub Action dispatches `repository_dispatch` to the super-repo with the service name and new SHA
|
||||
3. Super-repo detects the dispatch, waits for the SHA to be fetchable, then builds only that service
|
||||
4. The compose manifest is updated and committed with the new SHA tag
|
||||
5. The deploy workflow runs and updates only the changed containers
|
||||
|
||||
Install & enable:
|
||||
### Updating Submodules
|
||||
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/tailscale-routes.service > /dev/null << 'EOF'
|
||||
[Unit]
|
||||
Description=Add Tailscale routes to main routing table
|
||||
After=tailscaled.service
|
||||
Requires=tailscaled.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable tailscale-routes.service
|
||||
sudo systemctl start tailscale-routes.service
|
||||
# Update a single submodule to latest
|
||||
cd apps/scraper
|
||||
git checkout main
|
||||
git pull
|
||||
cd ../..
|
||||
git add apps/scraper
|
||||
git commit -m "chore(scraper): update submodule to latest"
|
||||
```
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
```bash
|
||||
# Cek Tailscale peers
|
||||
tailscale status
|
||||
|
||||
# Cek route table 52 (Tailscale internal)
|
||||
ip route show table 52
|
||||
|
||||
# Cek route table main (yang dipakai container)
|
||||
ip route show table main | grep 100.
|
||||
|
||||
# Test connectivity dari dalam container
|
||||
docker exec <container> node -e "
|
||||
const net = require('net');
|
||||
const c = new net.Socket();
|
||||
c.setTimeout(5000);
|
||||
c.connect(5432, '100.121.180.82', () => { console.log('OK'); c.end(); });
|
||||
c.on('error', e => { console.log('FAIL:', e.code); });
|
||||
c.on('timeout', () => { console.log('TIMEOUT'); c.destroy(); });
|
||||
"
|
||||
|
||||
# Cek service tailscale-routes
|
||||
systemctl status tailscale-routes.service
|
||||
```
|
||||
|
||||
## Menambahkan Aplikasi Baru
|
||||
|
||||
Panduan langkah demi langkah untuk menambahkan aplikasi baru ada di `docs/add-new-app.md`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT
|
||||
+1
-1
Submodule apps/hub updated: 51798f483f...9ea002a7a1
+1
-1
Submodule apps/llm-api updated: d8425ea3b3...5f7ead5503
+1
-1
Submodule apps/scraper updated: bc782ae4f8...62aa5b0e52
+1
-1
Submodule apps/tools updated: 3956b90c3c...036f67d05a
@@ -11,7 +11,7 @@ Dua node terhubung via **Tailscale** overlay network:
|
||||
│ Tailscale: 100.x.x.x │◄──────┤ │
|
||||
│ │ │ Layanan: │
|
||||
│ Layanan: │ │ ├─ PostgreSQL (port 6432) │
|
||||
│ ├─ Traefik (port 80/443) │ │ └─ Redis (port 6379) │
|
||||
│ ├─ Caddy (port 80/443) │ │ └─ Redis (port 6379) │
|
||||
│ ├─ NATS + JetStream │ │ │
|
||||
│ ├─ Dapr Placement │ └──────────────────────────────┘
|
||||
│ ├─ Redis (cache, Dapr) │
|
||||
@@ -30,7 +30,7 @@ Container di `orangevps` tidak bisa langsung mencapai IP Tailscale (`100.x.x.x`)
|
||||
Internet
|
||||
│
|
||||
▼ Port 443
|
||||
Traefik (v3.6)
|
||||
Caddy 2.11.4 (auto-TLS LE, HTTP/3)
|
||||
├─ TLS termination (sertifikat dari volume mount)
|
||||
├─ Middleware chain: secure-headers → compress → retry → rate-limit → buffer
|
||||
├─ Plugin: real-ip (Cloudflare), block-sensitive-paths
|
||||
@@ -40,11 +40,11 @@ Host(`asepharyana.my.id`) || Host(`www.asepharyana.my.id`) → hub
|
||||
host(`hub.asepharyana.my.id`) → hub (SPA + dashboard)
|
||||
Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`) → scraper-api
|
||||
│
|
||||
├─ hub (Next.js, port 3000)
|
||||
├─ hub (Next.js, port 4003)
|
||||
│ ├─ / — Portfolio SPA
|
||||
│ ├─ /dashboard — Ops dashboard (client-side, auto-refresh 15s)
|
||||
│ ├─ /api/dashboard — JSON: Docker containers, Jaeger traces, Prometheus metrics
|
||||
│ └─ Docker socket mounted (:ro) for container discovery
|
||||
│ ├─ /api/dashboard — JSON: systemd services, Jaeger traces, Prometheus metrics
|
||||
│ └─ Metrics via node-exporter + app endpoints
|
||||
│
|
||||
▼ Service load balancer
|
||||
http://scraper-api:4091
|
||||
@@ -71,7 +71,7 @@ Semua service berjalan dalam satu Docker Compose project bernama `compose` dan b
|
||||
| `nats.yml` | `nats` | Message broker + JetStream persistent streaming |
|
||||
| `dapr.yml` | `dapr-placement` | Koordinasi actor placement untuk sidecar Dapr |
|
||||
| `scraper.yml` | `scraper-api` + `scraper-api-dapr` | Aplikasi Rust + sidecar Dapr |
|
||||
| `hub.yml` | `hub` | Next.js SPA portfolio + dashboard + Docker socket |
|
||||
| systemd hub | `hub` | Next.js SPA portfolio + dashboard |
|
||||
| `observability.yml` | `otel-collector`, `jaeger`, `prometheus`, `node-exporter` | Tracing, metrics, observability |
|
||||
|
||||
### Dapr Sidecar Pattern
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Architecture
|
||||
|
||||
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
||||
|
||||
## System Overview
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Implementation Plan — Granular Task Breakdown
|
||||
|
||||
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
||||
|
||||
Setiap task adalah unit kerja terkecil yang bisa dikerjakan dalam 1-4 jam. Format:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Infrastructure & Deployment
|
||||
|
||||
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
||||
|
||||
## Docker Image Architecture
|
||||
|
||||
Project ini punya **satu Docker image** dengan multi-stage build. Backend Rust + Tesseract + ONNX model plus frontend Next.js.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Document Scanner — Processing Pipeline
|
||||
|
||||
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
||||
|
||||
Ini adalah inti dari project. Pipeline mengubah foto dokumen HP jadi dokumen scan yang proper. Setiap tahap dibahas detail teknisnya.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
Kumpulan solusi untuk masalah umum yang spesifik di infrastruktur `asepharyana-hub`.
|
||||
|
||||
> **Catatan (2026-08-02):** Produksi sekarang Caddy + Nix/systemd. Section Traefik/Docker di bawah adalah LEGACY — Docker dan Traefik dihapus dari produksi; gunakan hanya sebagai referensi historis.
|
||||
|
||||
## Daftar Isi
|
||||
|
||||
- [Deployment](#deployment)
|
||||
- [Dapr](#dapr)
|
||||
- [NATS](#nats)
|
||||
- [Traefik](#traefik)
|
||||
- [Tailscale / Networking](#tailscale--networking)
|
||||
- [Docker / Container](#docker--container)
|
||||
- [Caddy](#caddy)
|
||||
- [Database](#database)
|
||||
- [Submodule](#submodule)
|
||||
|
||||
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1785301185,
|
||||
"narHash": "sha256-eoS3KQTO0aPWXZvIaRbRAzSSHW3l5wdMFXtT1ISfoKA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9bc02893134c733dd85de46ee4fb2fac696b5529",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
description = "Asepharyana Hub — Nix builds for infrastructure and app services";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
|
||||
# ── mkApp generator ──
|
||||
mkApp = { name, src, buildScript, installScript, nativeBuildInputs ? [], buildInputs ? [] }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
inherit name src;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cacert curl gcc gnumake openssl pkg-config python3 libclang
|
||||
] ++ nativeBuildInputs;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
nodejs openssl stdenv.cc.cc.lib libffi
|
||||
] ++ buildInputs;
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
LD_LIBRARY_PATH = "${pkgs.libclang.lib}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.libffi}/lib";
|
||||
NIX_ENFORCE_PURITY = "0";
|
||||
|
||||
SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
NODE_EXTRA_CA_CERTS = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
NODE_ENV = "production";
|
||||
|
||||
phases = [ "unpackPhase" "buildPhase" "installPhase" ];
|
||||
buildPhase = ''
|
||||
export HOME="$TMPDIR" CARGO_HOME="$TMPDIR/.cargo-${name}"
|
||||
SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||
'' + buildScript;
|
||||
installPhase = installScript;
|
||||
};
|
||||
|
||||
# ── Node.js ──
|
||||
nodejs = pkgs.nodejs-slim_22;
|
||||
pnpm = pkgs.pnpm.override { inherit nodejs; };
|
||||
|
||||
# ── Common Rust build deps ──
|
||||
cargoDeps = with pkgs; [ rustc cargo clang cmake pkg-config openssl.dev zlib ];
|
||||
|
||||
# ── Submodule repos — URLs from .gitmodules ──
|
||||
submoduleRepos = {
|
||||
hub = "https://github.com/asepharyana/asepharyana-hub-hub.git";
|
||||
scraper = "https://github.com/asepharyana/asepharyana-hub-scraper.git";
|
||||
tools = "https://github.com/asepharyana/asepharyana-hub-tools.git";
|
||||
llm-api = "https://github.com/asepharyana/asepharyana-hub-llm-api.git";
|
||||
};
|
||||
|
||||
# ── Fetch submodule source ──
|
||||
submoduleSrc = name: builtins.fetchGit {
|
||||
url = submoduleRepos.${name};
|
||||
rev = if name == "hub" then "a90d0c43336a5f000b5856003d2293c420e7d595"
|
||||
else if name == "scraper" then "62aa5b0e52859afe3ba9de1c7b11cfe2dacf6c2c"
|
||||
else if name == "tools" then "3956b90c3ce39ffa7ffba8084937f20e11364d6b"
|
||||
else if name == "llm-api" then "5f7ead5503082a71d41a36fd1727325c784e4b79"
|
||||
else "HEAD";
|
||||
submodules = true;
|
||||
};
|
||||
|
||||
# ─── App Derivations ───
|
||||
hub = mkApp {
|
||||
name = "hub-0.1.0";
|
||||
src = submoduleSrc "hub";
|
||||
|
||||
nativeBuildInputs = with pkgs; [ bun ];
|
||||
|
||||
buildScript = ''
|
||||
echo "=== Installing dependencies ==="
|
||||
bun install 2>&1
|
||||
echo "=== Building Next.js ==="
|
||||
bun run build 2>&1
|
||||
'';
|
||||
|
||||
installScript = ''
|
||||
mkdir -p $out/share/hub $out/bin
|
||||
cp -r .next $out/share/hub/
|
||||
cp -r public $out/share/hub/ 2>/dev/null || true
|
||||
cp package.json $out/share/hub/
|
||||
cp next.config.{ts,mjs,js} $out/share/hub/ 2>/dev/null || true
|
||||
cp -r node_modules $out/share/hub/
|
||||
cat > $out/bin/hub << WRAPPER
|
||||
#!${pkgs.runtimeShell}
|
||||
exec ${pkgs.bun}/bin/bun run --cwd $out/share/hub start
|
||||
WRAPPER
|
||||
chmod +x $out/bin/hub
|
||||
'';
|
||||
};
|
||||
|
||||
scraper = mkApp {
|
||||
name = "scraper-0.1.0";
|
||||
src = submoduleSrc "scraper";
|
||||
nativeBuildInputs = cargoDeps;
|
||||
|
||||
buildScript = ''
|
||||
echo "=== Building scraper ==="
|
||||
cargo build --release 2>&1
|
||||
'';
|
||||
|
||||
installScript = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/scraper $out/bin/scraper
|
||||
'';
|
||||
};
|
||||
|
||||
tools-gateway = mkApp {
|
||||
name = "tools-gateway-0.1.0";
|
||||
src = submoduleSrc "tools";
|
||||
nativeBuildInputs = cargoDeps ++ [ pkgs.tesseract ];
|
||||
|
||||
buildScript = ''
|
||||
cd backend
|
||||
echo "=== Building tools-gateway ==="
|
||||
cargo build --release --features tesseract --bin tools-gateway 2>&1
|
||||
'';
|
||||
|
||||
installScript = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/tools-gateway $out/bin/tools-gateway
|
||||
'';
|
||||
};
|
||||
|
||||
tools-workers = mkApp {
|
||||
name = "tools-workers-0.1.0";
|
||||
src = submoduleSrc "tools";
|
||||
nativeBuildInputs = cargoDeps ++ [ pkgs.tesseract pkgs.leptonica ];
|
||||
|
||||
buildScript = ''
|
||||
cd backend
|
||||
echo "=== Building tools-workers ==="
|
||||
cargo build --release --features tesseract --bin tools-workers 2>&1
|
||||
'';
|
||||
installScript = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/tools-workers $out/bin/tools-workers
|
||||
'';
|
||||
};
|
||||
|
||||
tools-frontend = mkApp {
|
||||
name = "tools-frontend-0.1.0";
|
||||
src = submoduleSrc "tools";
|
||||
nativeBuildInputs = with pkgs; [ bun ];
|
||||
|
||||
buildScript = ''
|
||||
cd frontend
|
||||
echo "=== Installing dependencies ==="
|
||||
bun install 2>&1
|
||||
echo "=== Building Next.js ==="
|
||||
bun run build 2>&1
|
||||
'';
|
||||
|
||||
installScript = ''
|
||||
mkdir -p $out/share/tools-frontend $out/bin
|
||||
cp -r .next $out/share/tools-frontend/
|
||||
cp -r public $out/share/tools-frontend/ 2>/dev/null || true
|
||||
cp package.json $out/share/tools-frontend/
|
||||
cp -r node_modules $out/share/tools-frontend/
|
||||
cat > $out/bin/tools-frontend << WRAPPER
|
||||
#!${pkgs.runtimeShell}
|
||||
exec ${pkgs.bun}/bin/bun run --cwd $out/share/tools-frontend start
|
||||
WRAPPER
|
||||
chmod +x $out/bin/tools-frontend
|
||||
'';
|
||||
};
|
||||
|
||||
llm-api = mkApp {
|
||||
name = "llm-api-0.1.0";
|
||||
src = submoduleSrc "llm-api";
|
||||
nativeBuildInputs = cargoDeps ++ [ pkgs.cmake pkgs.gcc ];
|
||||
|
||||
buildScript = ''
|
||||
echo "=== Building llm-api ==="
|
||||
cargo build --release 2>&1
|
||||
'';
|
||||
|
||||
installScript = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/llm-api $out/bin/llm-api
|
||||
'';
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
inherit hub scraper tools-gateway tools-workers tools-frontend llm-api;
|
||||
default = hub;
|
||||
};
|
||||
|
||||
apps.hub = {
|
||||
type = "app";
|
||||
program = "${hub}/bin/hub";
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [ nodejs-slim_22 bun pnpm rustc cargo ];
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
# ── Caddyfile PRODUKSI v2 — TUNED (auto-TLS Let's Encrypt) ──
|
||||
# Tuning: HTTP/3 default, keep-alive upstream, zstd+gzip, timeouts, buffer, TLS 1.3
|
||||
|
||||
{
|
||||
email asepharyana@gmail.com
|
||||
|
||||
# Global tuning
|
||||
servers {
|
||||
protocols h1 h2 h3
|
||||
trusted_proxies static private_ranges
|
||||
}
|
||||
grace_period 10s
|
||||
}
|
||||
|
||||
# ── Helper: handler umum (gzip + zstd, header keamanan) — dipanggil dgn argumen: port
|
||||
# Penggunaan: import proxy 4003
|
||||
(proxy) {
|
||||
encode zstd gzip
|
||||
header {
|
||||
-Server
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
# Keep-alive upstream: max 100 idle conns per host, dial timeout 3s
|
||||
reverse_proxy 127.0.0.1:{args[0]} {
|
||||
transport http {
|
||||
keepalive 120s
|
||||
keepalive_interval 30s
|
||||
max_conns_per_host 100
|
||||
dial_timeout 3s
|
||||
response_header_timeout 30s
|
||||
read_timeout 60s
|
||||
write_timeout 60s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asepharyana.my.id, www.asepharyana.my.id, asepharyana.web.id, www.asepharyana.web.id, hub.asepharyana.my.id {
|
||||
import proxy 4003
|
||||
}
|
||||
|
||||
dashboard.asepharyana.my.id {
|
||||
import proxy 4013
|
||||
}
|
||||
|
||||
imphnen.asepharyana.my.id {
|
||||
import proxy 4009
|
||||
}
|
||||
|
||||
scraper.asepharyana.my.id, api.asepharyana.my.id, scraper.asepharyana.web.id, api.asepharyana.web.id {
|
||||
import proxy 4091
|
||||
}
|
||||
|
||||
ai.asepharyana.my.id, ai.asepharyana.web.id {
|
||||
import proxy 4010
|
||||
}
|
||||
|
||||
tools.asepharyana.my.id, tools.asepharyana.web.id {
|
||||
import proxy 4007
|
||||
}
|
||||
|
||||
9router.asepharyana.my.id {
|
||||
# LLM streaming: 9router combo models punya TTFT sampe 30-40s (deepseek,
|
||||
# fallback chain). Default (proxy) response_header_timeout 30s / read 60s
|
||||
# bikin false-positive 504 walau 9router masih ngolah. Longgarkan khusus
|
||||
# biar health-check & request PR-Agent real gak kena timeout transient.
|
||||
encode zstd gzip
|
||||
header {
|
||||
-Server
|
||||
X-Content-Type-Options "nosniff"
|
||||
}
|
||||
reverse_proxy 127.0.0.1:4014 {
|
||||
transport http {
|
||||
dial_timeout 3s
|
||||
response_header_timeout 120s
|
||||
read_timeout 300s
|
||||
write_timeout 300s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pr-agent.asepharyana.my.id {
|
||||
import proxy 4002
|
||||
}
|
||||
|
||||
lidm.asepharyana.my.id {
|
||||
import proxy 4004
|
||||
}
|
||||
|
||||
lidm-api.asepharyana.my.id {
|
||||
import proxy 4005
|
||||
}
|
||||
|
||||
zeavisedu.asepharyana.my.id {
|
||||
import proxy 4011
|
||||
}
|
||||
|
||||
api-zeavisedu.asepharyana.my.id {
|
||||
import proxy 4006
|
||||
}
|
||||
|
||||
ml-zeavisedu.asepharyana.my.id {
|
||||
import proxy 4012
|
||||
}
|
||||
|
||||
upload.asepharyana.my.id, upload.asepharyana.web.id {
|
||||
# Upload/download besar: JANGAN kompres, JANGAN limit body, flush instan (no buffering)
|
||||
header {
|
||||
-Server
|
||||
X-Content-Type-Options "nosniff"
|
||||
}
|
||||
request_body {
|
||||
max_size 0
|
||||
}
|
||||
reverse_proxy 127.0.0.1:4000 {
|
||||
transport http {
|
||||
dial_timeout 3s
|
||||
read_timeout 300s
|
||||
write_timeout 300s
|
||||
}
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
hub:
|
||||
container_name: hub
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/hub:sha-22e8a87
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/hub:sha-ac25a51
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
llm-api:
|
||||
container_name: llm-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/llm-api:sha-e953c3c
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/llm-api:sha-43f0df4
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- MODEL_PATH=/models/MiniCPM-V-4.6-Q4_K_M.gguf
|
||||
- MODEL_PATH=/models/MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-Q8_0.gguf
|
||||
- API_KEY=${LLM_API_KEY:-}
|
||||
volumes:
|
||||
- /root/models/gguf:/models:ro
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
scraper-api:
|
||||
container_name: scraper-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-e953c3c
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-2459541
|
||||
restart: always
|
||||
depends_on:
|
||||
nats:
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
tools:
|
||||
container_name: tools
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/tools:sha-e953c3c
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/tools:sha-ac25a51
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
@@ -13,6 +13,7 @@ services:
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
- '443:443/udp'
|
||||
networks:
|
||||
- app-shared-net
|
||||
extra_hosts:
|
||||
@@ -45,6 +46,13 @@ services:
|
||||
- '--entryPoints.websecure.transport.lifeCycle.requestAcceptGraceTimeout=15s'
|
||||
- '--entryPoints.websecure.transport.lifeCycle.graceTimeOut=10s'
|
||||
- '--entryPoints.websecure.address=:443'
|
||||
- '--entryPoints.websecure.http3=true'
|
||||
# ── Response speed tuning ──
|
||||
- '--serversTransport.maxIdleConnsPerHost=100'
|
||||
- '--serversTransport.forwardingTimeouts.dialTimeout=3s'
|
||||
- '--serversTransport.forwardingTimeouts.idleConnTimeout=180s'
|
||||
- '--global.checkNewVersion=false'
|
||||
- '--global.sendAnonymousUsage=false'
|
||||
- '--experimental.plugins.real-ip.moduleName=github.com/soulbalz/traefik-real-ip'
|
||||
- '--experimental.plugins.real-ip.version=v1.0.3'
|
||||
- '--experimental.plugins.blockpath.moduleName=github.com/traefik/plugin-blockpath'
|
||||
@@ -68,7 +76,7 @@ services:
|
||||
- GOGC=200
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${TRAEFIK_CONFIG_PATH:-/root/asepharyana-hub/infra/traefik/dynamic}:/etc/traefik/dynamic:ro
|
||||
- ${TRAEFIK_CONFIG_PATH:-/home/code/asepharyana-hub/infra/traefik/dynamic}:/etc/traefik/dynamic:ro
|
||||
- ${TRAEFIK_CERT_MY_ID_PEM:-/root/asepharyana.my.id.pem}:/etc/traefik/certs/asepharyana.my.id.pem:ro
|
||||
- ${TRAEFIK_CERT_MY_ID_KEY:-/root/asepharyana.my.id.key}:/etc/traefik/certs/asepharyana.my.id.key:ro
|
||||
- ${TRAEFIK_CERT_WEB_ID_PEM:-/root/asepharyana.web.id.pem}:/etc/traefik/certs/asepharyana.web.id.pem:ro
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# OrangeVPS hardening sysctl — /etc/sysctl.d/99-hardening.conf
|
||||
# Network hardening
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
net.ipv4.conf.all.rp_filter = 1
|
||||
net.ipv4.conf.default.rp_filter = 1
|
||||
net.ipv4.conf.all.accept_redirects = 0
|
||||
net.ipv4.conf.default.accept_redirects = 0
|
||||
net.ipv4.conf.all.send_redirects = 0
|
||||
net.ipv4.conf.default.send_redirects = 0
|
||||
net.ipv4.conf.all.accept_source_route = 0
|
||||
net.ipv4.conf.default.accept_source_route = 0
|
||||
net.ipv6.conf.all.accept_redirects = 0
|
||||
net.ipv6.conf.default.accept_redirects = 0
|
||||
net.ipv6.conf.all.accept_source_route = 0
|
||||
net.ipv6.conf.default.accept_source_route = 0
|
||||
|
||||
# TCP hardening
|
||||
net.ipv4.tcp_max_syn_backlog = 4096
|
||||
net.ipv4.tcp_synack_retries = 2
|
||||
net.ipv4.tcp_syn_retries = 3
|
||||
net.ipv4.tcp_fin_timeout = 15
|
||||
net.ipv4.tcp_keepalive_time = 120
|
||||
net.ipv4.tcp_keepalive_intvl = 30
|
||||
net.ipv4.tcp_keepalive_probes = 5
|
||||
net.ipv4.tcp_tw_reuse = 1
|
||||
net.ipv4.ip_local_port_range = 1024 65535
|
||||
|
||||
# Kernel hardening
|
||||
kernel.randomize_va_space = 2
|
||||
kernel.core_uses_pid = 1
|
||||
kernel.dmesg_restrict = 1
|
||||
kernel.kptr_restrict = 1
|
||||
kernel.yama.ptrace_scope = 2
|
||||
fs.protected_hardlinks = 1
|
||||
fs.protected_symlinks = 1
|
||||
fs.suid_dumpable = 0
|
||||
|
||||
# Resource limits (SYN flood protection)
|
||||
net.core.somaxconn = 1024
|
||||
net.core.netdev_max_backlog = 4096
|
||||
@@ -0,0 +1,12 @@
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
MaxStartups 100:30:200
|
||||
MaxSessions 100
|
||||
TCPKeepAlive yes
|
||||
|
||||
# Hardening 2026-08-02
|
||||
MaxAuthTries 4
|
||||
LoginGraceTime 30
|
||||
PermitRootLogin prohibit-password
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding yes
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# firewall.sh — deny-by-default firewall untuk orangevps
|
||||
# Public: 22 (SSH), 80/443 (Caddy), 4013 (hermes dashboard)
|
||||
# 25565 (Minecraft) — WHITELIST TCPShield proxy only
|
||||
# Tailscale CGNAT 100.64/10: semua port (imrnes & node lain)
|
||||
# Localhost: semua
|
||||
# Sisanya: DROP + log
|
||||
# ============================================================
|
||||
# TCPShield proxy ranges (https://tcpshield.com/v4/ + /v4-cf/)
|
||||
# Update saat TCPShield publish range baru.
|
||||
TCPSHIELD_V4=(
|
||||
198.178.119.0/24
|
||||
104.234.6.0/24
|
||||
)
|
||||
TCPSHIELD_V4_CF=(
|
||||
89.222.122.36/31
|
||||
152.233.22.8/31
|
||||
89.222.108.246/31
|
||||
84.17.55.186/31
|
||||
51.79.45.52/31
|
||||
5.135.84.92/30
|
||||
51.75.35.44/30
|
||||
51.161.27.110/31
|
||||
152.233.30.16/31
|
||||
152.233.30.232/31
|
||||
203.205.31.160/31
|
||||
)
|
||||
set -e
|
||||
|
||||
### IPv4 ###
|
||||
iptables -F
|
||||
iptables -X
|
||||
iptables -Z
|
||||
|
||||
# Policy default DROP
|
||||
iptables -P INPUT DROP
|
||||
iptables -P FORWARD DROP
|
||||
iptables -P OUTPUT ACCEPT
|
||||
|
||||
# Loopback
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
|
||||
# Established/related
|
||||
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Tailscale overlay (100.64.0.0/10) — imrnes & peers
|
||||
iptables -A INPUT -s 100.64.0.0/10 -j ACCEPT
|
||||
|
||||
# Public: SSH, HTTP(S)
|
||||
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
# Public: hermes dashboard (auth-protected)
|
||||
iptables -A INPUT -p tcp --dport 4013 -j ACCEPT
|
||||
# Public: Minecraft (FTB sky) — hanya dari proxy TCPShield
|
||||
for cidr in "${TCPSHIELD_V4[@]}" "${TCPSHIELD_V4_CF[@]}"; do
|
||||
iptables -A INPUT -s "$cidr" -p tcp --dport 25565 -j ACCEPT
|
||||
done
|
||||
|
||||
# ICMP (ping, PMTU)
|
||||
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/sec --limit-burst 10 -j ACCEPT
|
||||
iptables -A INPUT -p icmp -m conntrack --ctstate NEW -j ACCEPT
|
||||
iptables -A INPUT -p icmp -j ACCEPT
|
||||
|
||||
# Log dropped (rate-limited, 1 baris/5s)
|
||||
iptables -A INPUT -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "FW-DROP " --log-level 4
|
||||
iptables -A INPUT -j DROP
|
||||
|
||||
# UFW chains (dipanggil dari ts-input) — kosongkan
|
||||
iptables -F ufw-before-input 2>/dev/null || true
|
||||
iptables -F ufw-after-input 2>/dev/null || true
|
||||
iptables -F ufw-before-logging-input 2>/dev/null || true
|
||||
iptables -F ufw-after-logging-input 2>/dev/null || true
|
||||
iptables -F ufw-reject-input 2>/dev/null || true
|
||||
iptables -F ufw-track-input 2>/dev/null || true
|
||||
|
||||
### IPv6 ###
|
||||
ip6tables -F
|
||||
ip6tables -X
|
||||
ip6tables -Z
|
||||
ip6tables -P INPUT DROP
|
||||
ip6tables -P FORWARD DROP
|
||||
ip6tables -P OUTPUT ACCEPT
|
||||
ip6tables -A INPUT -i lo -j ACCEPT
|
||||
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
# Tailscale IPv6 ULA (fd7a:115c::/48)
|
||||
ip6tables -A INPUT -s fd7a:115c::/48 -j ACCEPT
|
||||
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
ip6tables -A INPUT -p tcp --dport 4013 -j ACCEPT
|
||||
# Minecraft 25565: TCPShield IPv4 only — tidak ada range IPv6 publik
|
||||
ip6tables -A INPUT -p icmpv6 -j ACCEPT
|
||||
ip6tables -A INPUT -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "FW6-DROP " --log-level 4
|
||||
ip6tables -A INPUT -j DROP
|
||||
|
||||
echo "Firewall applied:"
|
||||
iptables -L INPUT -n --line-numbers | head -24
|
||||
@@ -5,52 +5,37 @@ receivers:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
exporters:
|
||||
otlp/jaeger:
|
||||
endpoint: jaeger:4317
|
||||
tls:
|
||||
insecure: true
|
||||
|
||||
nop:
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
enable_open_metrics: true
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
|
||||
debug:
|
||||
verbosity: normal
|
||||
sampling_initial: 5
|
||||
sampling_thereafter: 100
|
||||
|
||||
processors:
|
||||
batch:
|
||||
timeout: 1s
|
||||
send_batch_size: 1024
|
||||
|
||||
memory_limiter:
|
||||
check_interval: 1s
|
||||
limit_mib: 512
|
||||
spike_limit_mib: 128
|
||||
|
||||
attributes:
|
||||
actions:
|
||||
- key: service.namespace
|
||||
value: asepharyana-hub
|
||||
action: upsert
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
service:
|
||||
extensions: [health_check]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [memory_limiter, batch, attributes]
|
||||
exporters: [otlp/jaeger, debug]
|
||||
exporters: [nop]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [memory_limiter, batch, attributes]
|
||||
exporters: [prometheus, debug]
|
||||
exporters: [prometheus]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Nix service targets (file_sd) — 2026-08-02
|
||||
# Service yang expose /metrics langsung:
|
||||
- targets: ['127.0.0.1:4016']
|
||||
labels:
|
||||
service: gmw-discord-gateway
|
||||
- targets: ['127.0.0.1:4008']
|
||||
labels:
|
||||
service: tools-gateway
|
||||
# GMW backend expose /api/metrics (bukan /metrics):
|
||||
- targets: ['127.0.0.1:4001']
|
||||
labels:
|
||||
service: gmw-backend
|
||||
__metrics_path__: /api/metrics
|
||||
# llm-api expose /metrics sejak 2026-08-03 (feat metrics):
|
||||
- targets: ['127.0.0.1:4010']
|
||||
labels:
|
||||
service: llm-api
|
||||
@@ -0,0 +1,4 @@
|
||||
[Service]
|
||||
Environment=OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
|
||||
Environment=OTEL_SERVICE_NAME=scraper
|
||||
Environment=OTEL_METRICS_EXPORT_INTERVAL=5000
|
||||
@@ -27,7 +27,7 @@ http:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
- llm-chain@file
|
||||
service: llm-api-service
|
||||
|
||||
# ── Tools (Document Scanner & Media Processing) ──
|
||||
@@ -50,28 +50,178 @@ http:
|
||||
- common-chain@file
|
||||
service: jaeger-service
|
||||
|
||||
# ── 9Router (AI routing gateway) ──
|
||||
9router:
|
||||
rule: 'Host(`9router.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: 9router-service
|
||||
|
||||
# ── PR-Agent (GitHub App webhook) ──
|
||||
pr-agent:
|
||||
rule: 'Host(`pr-agent.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: pr-agent-service
|
||||
|
||||
# ── LIDM Frontend ──
|
||||
lidm-frontend:
|
||||
rule: 'Host(`lidm.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: lidm-frontend-service
|
||||
|
||||
# ── LIDM Backend API ──
|
||||
lidm-backend:
|
||||
rule: 'Host(`lidm-api.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: lidm-backend-service
|
||||
|
||||
# ── ZeaVis Edu Frontend ──
|
||||
zeavisedu:
|
||||
rule: 'Host(`zeavisedu.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: zeavisedu-service
|
||||
|
||||
# ── ZeaVis Edu API ──
|
||||
api-zeavisedu:
|
||||
rule: 'Host(`api-zeavisedu.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: api-zeavisedu-service
|
||||
|
||||
# ── ZeaVis Edu ML Service ──
|
||||
ml-zeavisedu:
|
||||
rule: 'Host(`ml-zeavisedu.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: ml-zeavisedu-service
|
||||
|
||||
# ── Hermes Dashboard ──
|
||||
hermes-dashboard:
|
||||
rule: 'Host(`dashboard.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: hermes-dashboard-service
|
||||
|
||||
# ── GMW Discord Automod Dashboard (Nix: gmw-proxy on 8080) ──
|
||||
gmw:
|
||||
rule: 'Host(`imphnen.asepharyana.my.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: gmw-service
|
||||
|
||||
# ── TeleUploader (S3 to Telegram Bridge, Nix: bun on 3000) ──
|
||||
teleuploader:
|
||||
rule: 'Host(`upload.asepharyana.my.id`) || Host(`upload.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- upload-chain@file
|
||||
service: teleuploader-service
|
||||
|
||||
services:
|
||||
hub-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://hub:3000'
|
||||
- url: 'http://host.docker.internal:3099'
|
||||
|
||||
scraper-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://scraper-api:4091'
|
||||
- url: 'http://host.docker.internal:4091'
|
||||
|
||||
tools-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://tools:3000'
|
||||
- url: 'http://host.docker.internal:3500'
|
||||
|
||||
llm-api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://llm-api:8080'
|
||||
- url: 'http://host.docker.internal:8082'
|
||||
|
||||
jaeger-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://jaeger:16686'
|
||||
|
||||
9router-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:20128'
|
||||
|
||||
pr-agent-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:3002'
|
||||
|
||||
lidm-frontend-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:3100'
|
||||
|
||||
lidm-backend-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:3101'
|
||||
|
||||
zeavisedu-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:8088'
|
||||
|
||||
api-zeavisedu-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:3200'
|
||||
|
||||
ml-zeavisedu-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:8200'
|
||||
|
||||
hermes-dashboard-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:9119'
|
||||
|
||||
gmw-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:8080'
|
||||
|
||||
teleuploader-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://host.docker.internal:3000'
|
||||
|
||||
@@ -61,6 +61,14 @@ http:
|
||||
- "^/wp-login\\.php"
|
||||
- "^/config\\.php"
|
||||
|
||||
# ── LLM Stream Chain (no buffer/compress — SSE needs real-time) ──
|
||||
llm-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
- secure-headers
|
||||
- retry
|
||||
- rate-limit
|
||||
|
||||
# ── Common Chain ──
|
||||
common-chain:
|
||||
chain:
|
||||
@@ -70,3 +78,23 @@ http:
|
||||
- retry
|
||||
- rate-limit
|
||||
- buffer
|
||||
|
||||
# ── TeleUploader Chain (2GB body buffer — large file uploads) ──
|
||||
upload-buffer:
|
||||
buffering:
|
||||
maxRequestBodyBytes: 2147483648
|
||||
maxResponseBodyBytes: 2147483648
|
||||
memRequestBodyBytes: 1048576
|
||||
memResponseBodyBytes: 1048576
|
||||
upload-rate-limit:
|
||||
rateLimit:
|
||||
average: 300
|
||||
burst: 100
|
||||
period: 1m
|
||||
upload-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
- secure-headers
|
||||
- retry
|
||||
- upload-rate-limit
|
||||
- upload-buffer
|
||||
|
||||
+1
-1
Submodule plugins/hub-guide updated: abd60c607d...ffcd3ba8e3
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── Nix Deploy — VPS-side script ──
|
||||
# Runs after `nix copy --to ssh://VPS ./result` from CI
|
||||
# Usage: sudo ./deploy.sh <service-name>
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE="$1"
|
||||
PROFILE="/nix/var/nix/profiles/${SERVICE}"
|
||||
|
||||
# Find the latest store path for this service
|
||||
LATEST=$(ls -1d /nix/store/*-"${SERVICE}"-* 2>/dev/null | tail -1)
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo "ERROR: No store path found for ${SERVICE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update profile
|
||||
/nix/var/nix/profiles/default/bin/nix-env --profile "$PROFILE" --set "$LATEST"
|
||||
|
||||
# Restart service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "${SERVICE}" 2>/dev/null || systemctl restart "${SERVICE}"
|
||||
|
||||
echo "Deployed ${SERVICE}: ${LATEST}"
|
||||
systemctl is-active "${SERVICE}"
|
||||
|
||||
# Collect garbage (safe: only removes unreachable paths)
|
||||
# nix-collect-garbage -d 2>/dev/null || true
|
||||
Reference in New Issue
Block a user