102 Commits
Author SHA1 Message Date
monrepo-bot 59e6c5c6a1 chore: update hub to 9ea002a7a10d 2026-08-10 00:24:27 +00:00
monrepo-bot 4f8a975337 chore: update hub to e67db071bb0f 2026-08-10 00:24:15 +00:00
monrepo-bot c251867f05 chore: update hub to 17a785c9d277 2026-08-10 00:18:32 +00:00
monrepo-bot b06cddca4a chore: update hub to 915d8482947a 2026-08-10 00:18:27 +00:00
monrepo-bot 19aface599 chore: update hub to a5bf846e52fd 2026-08-10 00:18:19 +00:00
monrepo-bot 528a109d53 chore: update hub to eb80d4a0fd73 2026-08-07 00:10:49 +00:00
asepharyanaandAsep Haryana 97b635e8c6 firewall: whitelist TCPShield proxy ranges untuk port 25565 (Minecraft) 2026-08-06 20:42:39 +07:00
mytheclipsebotreview[bot]andGitHub 99e438d9c5 Auto-merge PR #10
chore(deps): bump actions/checkout from 6 to 7 in the github-actions group
2026-08-06 13:28:09 +00:00
dependabot[bot]andGitHub 8e850f05c4 chore(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 13:16:00 +00:00
asepharyana 6c2a4fc9d2 fix(infra): sync Caddyfile.prod with live — add dashboard.asepharyana.my.id block
dashboard was proxied via wildcard but missing from live Caddy site
blocks -> Cloudflare 525. Live /etc/caddy/Caddyfile is now the
authoritative copy; repo reference updated to match byte-for-byte so
the drift cannot recur.
2026-08-05 09:44:26 +07:00
Asep Haryana 3e92f68d78 fix(infra): revert imphnen block ke gmw-proxy 4009 (dimentorin gak dipublish via domain ini) 2026-08-04 23:12:02 +07:00
Asep Haryana 2050129062 fix(infra): imphnen site block — SPA :5173 + API /v1 → :4099 (was 525: no block on live caddy) 2026-08-04 22:51:09 +07:00
asepharyana a6148753ca fix(infra): 9router Caddy timeouts — false 504 on slow LLM TTFT
9router combo models (deepseek-v4-flash-free fallback) have TTFT up to
30-40s. Default (proxy) response_header_timeout 30s / read 60s caused
false-positive 504 'timeout awaiting response headers' even while 9router
was processing. Give the 9router route dedicated long timeouts
(response_header 120s, read/write 300s). Syncs /etc/caddy/Caddyfile.
2026-08-04 22:02:09 +07:00
asepharyanaandAsep Haryana d02989f1a7 ci: add Nix GC cleanup job on VPS after deploy 2026-08-04 13:58:10 +07:00
monrepo-bot 6e317eb05a chore: update hub to 55310768a5ac 2026-08-04 02:39:29 +00:00
asepharyana 4e114edb89 chore: update settings.json with new hooks and commands; update .gitignore to include .serena/ directory 2026-08-03 16:49:28 +07:00
aseph 49123c08b3 ci: use free GHA Nix cache (disable FlakeHub cache, not subscribed) 2026-08-03 16:44:32 +07:00
asepharyanaandAsep Haryana 539b5b8320 ci: enable FlakeHub Cache (id-token: write + use-flakehub) 2026-08-03 16:24:48 +07:00
monrepo-bot 39df52aab7 chore: update hub to ad6b1847c563 2026-08-03 09:23:16 +00:00
Asep Haryana b326911478 chore(infra): bump hub flake rev to a90d0c4 (error rate fix) 2026-08-03 13:07:38 +07:00
monrepo-bot 8242325ffd chore: update hub to a90d0c43336a 2026-08-03 06:06:50 +00:00
Asep Haryana c075cf282b fix(infra): correct hub flake rev to 62966fd (full sha) 2026-08-03 12:48:14 +07:00
Asep Haryana 93936efd40 chore(infra): bump hub flake rev to 62966fd (LLM dashboard card) 2026-08-03 12:46:11 +07:00
monrepo-bot 5f66cfc12b chore: update hub to 62966fd7cf45 2026-08-03 05:45:45 +00:00
Asep Haryana cc8c3f1bf3 chore(infra): add llm-api to prometheus targets.yml (now exposes /metrics) 2026-08-03 11:56:35 +07:00
Asep Haryana 0c36007da4 chore(infra): bump llm-api flake rev to 5f7ead5 2026-08-03 11:46:08 +07:00
monrepo-bot b7d02647d9 chore: update llm-api to 5f7ead550308 2026-08-03 04:45:04 +00:00
Asep HaryanaandClaude Opus 5 ade2798ded chore(infra): bump llm-api flake rev to 7cec411
Fixes streaming reasoning/content split (robust boundary detection,
flush un-tagged output) and renders reasoning in the chat UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:04:18 +07:00
monrepo-bot ef14ca431f chore: update llm-api to 7cec411cba7c 2026-08-03 04:04:04 +00:00
Asep HaryanaandClaude Opus 5 f681e624b3 chore(infra): bump llm-api flake rev to b636496
Deploys the generation-flow refactor (unified generate, streaming fixes,
tool-calling, strict model validation, AddBos::Never).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:50:23 +07:00
Asep HaryanaandClaude Opus 5 fdbdcbc1ec fix(ci): trigger nix deploy on every push to main
GitHub path filters do not match submodule gitlink changes, so the
`paths: apps/**` filter meant a submodule pointer update never triggered
the deploy. Drop the filter so any push to main deploys (matches the
documented "Push to main -> nix build -> systemctl restart").

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:35:56 +07:00
Asep HaryanaandClaude Opus 5 b7b60e9125 chore(ci): remove legacy Docker workflows (moved to Nix)
Docker was decommissioned 2026-08-02 but docker-build-push.yml and
deploy-docker.yml were left behind. They still listened to
repository_dispatch: [submodule-updated], so every app push queued a
redundant Docker build alongside the intended Nix deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:13:38 +07:00
monrepo-bot 722709f6fb chore: update llm-api to b63649649758 2026-08-03 01:58:50 +00:00
Asep Haryana d20643fbbd fix(infra): ship next.config.ts into Nix store for hub
next start re-reads next.config.ts at runtime; the OLD store had only
.next/public/package.json/node_modules, so images.maximumDiskCacheSize
fell back to defaults (undefined) and the image-optimizer disk LRU did
mkdir(<store>/.next/cache/images) -> EACCES on every optimized image.
Also now that next.config.ts sets maximumDiskCacheSize:0 +
isrFlushToDisk:false, shipping the file makes runtime match the baked
build config, eliminating the EACCES spike.
2026-08-03 04:33:46 +07:00
Asep Haryana 7da1727603 fix(firewall): accept IPv6 MLD/ND multicast (ff02::1, ff02::2, ff02::fb)
Neighbor multicast to ff02::1 (MLDv2 reports from other hosts) was
hitting the LOG+DROP tail — ~1800 FW6-DROP lines/6h in journald,
i.e. ~5/min of pure log spam from ICMPv6 layer-2 discovery. Accept
link-local multicast ranges before the logging rule.
2026-08-03 04:06:32 +07:00
Asep Haryana 9e9fd229d0 chore(infra): bump hub flake rev to 6829c7e (image disk cache fix) 2026-08-03 04:05:23 +07:00
Asep Haryana 40095f75f4 chore(infra): bump hub submodule to 6829c7efe2a735d249f5135bff4a0fc28411a295 (disable Next image disk cache) 2026-08-03 04:05:15 +07:00
asepharyana 3781a8882d chore(infra): scraper OTEL env drop-in (OTLP endpoint 127.0.0.1:4317) 2026-08-02 21:05:20 +07:00
asepharyana da2f4a2f2c chore(infra): prometheus file_sd targets for nix services (gmw-backend/api-metrics) 2026-08-02 21:05:12 +07:00
asepharyana ad22a39a9e chore(infra): use full sha for hub flake rev 2026-08-02 20:56:16 +07:00
asepharyanaandAsep Haryana 2f969972a1 chore(infra): bump hub flake rev to f35edb0 (dashboard fix) 2026-08-02 20:55:55 +07:00
monrepo-bot 620f06fbe0 chore: update hub to f35edb00d726 2026-08-02 13:55:07 +00:00
asepharyana e6033c8b12 feat(infra): hardening firewall + sysctl + ssh
- deny-by-default iptables/ip6tables: public cuma 22/80/443/4013,
  sisanya localhost + tailscale CGNAT; log FW-DROP rate-limited
- persist via netfilter-persistent (rules.v4/v6) + /usr/local/bin/firewall.sh
- sysctl hardening: syncookies, rp_filter, redirect off, kptr/dmesg
  restrict, protected links, somaxconn, tcp retries tuner
- sshd: PasswordAuthentication no (fix 50-cloud-init.conf override),
  PermitRootLogin prohibit-password, MaxAuthTries 4, LoginGraceTime 30,
  X11Forwarding no
2026-08-02 20:22:49 +07:00
asepharyana 500af023de fix(infra): otel collector no self-loop trace export
- hapus otlp/jaeger exporter (jaeger mati sejak Docker dihapus) yang
  nunjuk ke 127.0.0.1:4317 (port receiver sendiri) -> loop trace tanpa
  henti, ~1.8jt baris log/hari di journal
- traces pipeline -> nop exporter, metrics -> prometheus
- logs pipeline dihapus (tidak ada konsumen)
2026-08-02 19:59:31 +07:00
Asep Haryana e280d196f9 chore: update llm-api to 344bc195fa95 2026-08-02 17:02:59 +07:00
Asep Haryana 4927338c98 docs: sync infra docs to Caddy+Nix 4000s (Traefik/Docker legacy) 2026-08-02 16:54:17 +07:00
monrepo-bot 54ee903647 chore: update llm-api to 344bc195fa95 2026-08-02 09:45:06 +00:00
Asep Haryana 35cbcdcc92 chore(infra): add tuned Caddyfile.prod reference 2026-08-02 16:37:05 +07:00
monrepo-bot 324fe08913 chore: update llm-api to 81c51772495c 2026-08-02 09:18:56 +00:00
Asep Haryana e171201b1c chore: sync port references and docs to 4000s infra 2026-08-02 16:16:40 +07:00
Asep Haryana ec1394bdc5 chore: bump tools submodule (ports 4007/4008) 2026-08-02 14:31:47 +07:00
asepharyana f0787b5f23 fix(nix): restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) 2026-08-01 18:03:44 +07:00
asepharyana 0df6c35d5a ci: publish flake to FlakeHub (rolling) 2026-08-01 17:58:22 +07:00
Asep Haryana 5d21848e87 perf(infra): traefik response speed - HTTP/3 + backend conn pooling
- entryPoints.websecure.http3: enable QUIC (UDP 443, alt-svc h3)
- serversTransport maxIdleConnsPerHost 2->100: reuse backend keep-alive
- forwardingTimeouts.dialTimeout 30s->3s: fail fast on dead backends
- disable version-check/anonymous-usage network chatter
2026-08-01 13:12:50 +07:00
Asep Haryana b96548bffa fix(infra): raise upload-buffer response cap to 2GB
Chunked file streaming (TeleUploader) serves multi-part bodies up to
120MB+; 10MB maxResponseBodyBytes made every big download 500.
2026-08-01 12:52:34 +07:00
Asep Haryana e19fa76dc3 fix(infra): add teleuploader traefik router for upload hostnames
Restore upload.asepharyana.my.id/.web.id routing lost during Nix migration
(docker label router removed with container). Upload chain uses 2GB body
buffer + dedicated 300/100 rate limit, service targets bun on host:3000.
2026-08-01 12:34:22 +07:00
Asep Haryana c535ee7858 ci(otel): add logs pipeline to otel-collector (OTLP /v1/logs 404 fix) 2026-07-31 14:13:56 +07:00
Asep Haryana 80b5161630 ci(infra): traefik dynamic config path -> /home/code (drop /root dependency) 2026-07-31 14:02:07 +07:00
Asep Haryana 1ccc0355ac ci(traefik): add hermes dashboard route via file provider 2026-07-31 13:50:46 +07:00
asepharyana 12aed642a9 ci(traefik): add zeavis routes via file provider (Nix migration) 2026-07-31 12:42:14 +07:00
asepharyana f8e3dccb44 ci(traefik): lidm routes -> host ports 3100/3101 (3000/3001 occupied by teleuploader+gmw) 2026-07-31 11:37:13 +07:00
asepharyana 1811c6adc2 ci(traefik): add lidm frontend+backend routes via file provider (Nix migration) 2026-07-31 11:29:47 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ceeac5c02f chore(deps): bump the github-actions group across 1 directory with 3 updates (#9)
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) and [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `DeterminateSystems/nix-installer-action` from 16 to 22
- [Release notes](https://github.com/determinatesystems/nix-installer-action/releases)
- [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v16...v22)

Updates `DeterminateSystems/magic-nix-cache-action` from 8 to 14
- [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases)
- [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/v8...v14)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/magic-nix-cache-action
  dependency-version: '14'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/nix-installer-action
  dependency-version: '22'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 22:34:36 +07:00
Asep Haryana d0c9f16d5d chore(infra): disable Docker compose for Nix-migrated services (hub, scraper, tools, llm-api) 2026-07-30 22:29:20 +07:00
Asep Haryana 6cbafbfe2e fix(flake): update scraper pinned rev for config fix 2026-07-30 22:28:24 +07:00
GitHub Action d6b9a0806b chore: update manifests and submodules [skip ci] 2026-07-30 15:23:34 +00:00
Asep Haryana 60a1af436f fix(infra): llm-api port 8080→8082 (conflict with gmw-proxy nginx) 2026-07-30 22:18:56 +07:00
Asep Haryana 2459541677 fix(scraper): update submodule - config list_separator fix 2026-07-30 22:18:03 +07:00
Asep Haryana 33b999d2b7 fix(ci): disable Determinate Nix / FlakeHub (no flakehub flakes used)
DeterminateSystems/nix-installer-action defaults to determinate:true
which tries to auth with FlakeHub via GitHub JWT. We don't use any
FlakeHub flakes, so disable it — skipping the auth entirely.
2026-07-30 22:05:41 +07:00
Asep Haryana 59f131f951 fix(ci): combine build+deploy per-service, pass exact store path
Separate deploy job failed because it used ls to find store paths
by name, finding OLD local paths instead of the freshly copied CI
paths. Now each service builds, copies, and updates its profile
in a single job using the exact store path from the build output.
No more guessing which path is the right one.
2026-07-30 21:42:39 +07:00
Asep Haryana cc8c5088a4 fix(ci): use SSH_PRIVATE_KEY secret, sanitize key format
- Ganti secret name: VPS_SSH_KEY → SSH_PRIVATE_KEY (nama yg ada)
- Fix nix copy URL: ***@ → $VPS_USER@
- Sanitize SSH key: strip \r\n, validasi dengan ssh-keygen
- Cegah libcrypto error dari key format broken
2026-07-30 21:32:40 +07:00
Asep Haryana d15e8621a9 fix(ci): proper SSH user in nix copy, deploy job structure
- Fix nix copy URL: ***@ → $VPS_USER@
- Store path from build output, passed across jobs
- Deploy job waits for all builds via needs: build
- SSH key setup in its own step, guarded by main branch
- Only deploy on main branch pushes
- Remote deploy script fetches from VPS nix store
2026-07-30 21:14:20 +07:00
Asep Haryana 33d5b12ec6 fix: tools-workers install path (no cd backend in installPhase) 2026-07-30 20:11:31 +07:00
Asep Haryana b5596e1398 feat(infra): full Nix migration — all 6 services + CI/CD
- flake.nix: 6 derivations (hub, scraper, tools-gateway, tools-workers, tools-frontend, llm-api)
- Fetch submodule source via builtins.fetchGit with pinned revs
- Fix cargo HOME/TMPDIR for Nix sandbox permission issues
- Fix llm-api: CMake/Clang deps for llama.cpp-sys2 bindgen
- Add LIBCLANG_PATH, LD_LIBRARY_PATH for Rust bindgen builds
- Systemd units: tools-gateway (3501), tools-frontend (3500), tools-workers, llm-api (8080)
- tools.target for grouped management
- Env configs: /etc/tools/env, /etc/llm-api/env
- GitHub Actions: nix-build.yml — matrix build + nix copy + deploy
- Update Traefik apps.yaml: tools/host.docker.internal:3500, llm-api/host.docker.internal:8080
- iptables: allow Docker→host on 3099, 4091, 3500, 3501, 8080
- Add scripts/nix-deploy.sh for CI/CD deploy step
2026-07-30 19:45:43 +07:00
Asep Haryana 46730ec07d feat(infra): Nix build for scraper, GitHub Actions workflow
- Build scraper (Rust) with Nix — cargo build --release
- Create scraper systemd unit (port 4091), env from Docker config
- Fix HOME/CARGO_HOME for Rust/cargo in Nix sandbox
- Update Traefik apps.yaml: scraper -> host.docker.internal:4091
- Add iptables rules for port 4091 (Docker->host)
- Add GitHub Actions workflow: nix-build.yml (determinate-nix + deploy)
- Save iptables rules persistently
2026-07-30 18:46:45 +07:00
Asep Haryana 1ae8a53cd4 chore: ignore Nix build result symlink 2026-07-30 18:33:52 +07:00
Asep Haryana ff52f8841c feat(infra): Nix build for hub app, systemd deployment
- Add flake.nix with derivations for hub (Next.js), scraper, tools, llm-api
- Create hub systemd unit (port 3099)
- Update Traefik dynamic config to point to host hub service
- Add iptables rule for Docker-to-host communication
- Use fetchGit for submodule source resolution
2026-07-30 18:33:43 +07:00
monrepo-bot b747ae5c3b chore: update hub to b007ced615dc 2026-07-29 00:12:03 +00:00
monrepo-bot 64ce812b4e chore: update hub to 5370fd47954d 2026-07-29 00:10:06 +00:00
monrepo-bot 9837d9dc50 chore: update hub to 407aa7b8993b 2026-07-28 00:11:32 +00:00
monrepo-bot 86dda74174 chore: update hub to a803849cba6c 2026-07-27 00:21:20 +00:00
monrepo-bot d045602f3e chore: update hub to 6042a6056a99 2026-07-27 00:17:20 +00:00
GitHub Action 32049c70ac chore: update manifests and submodules [skip ci] 2026-07-26 12:18:15 +00:00
asepharyana be7348a27d fix(infra): remove buffer/compress from llm-api for real SSE streaming
- llm-api uses llm-chain instead of common-chain
- llm-chain excludes buffer & compress middlewares
- Prevents Traefik from buffering streaming SSE responses
- Each token is now flushed immediately
2026-07-26 19:14:52 +07:00
asepharyana 43f0df4493 fix(infra): remove buffer/compress from llm-api middleware chain
- Buffer middleware causes SSE stream buffering
- Compress middleware breaks real-time streaming
- Added llm-chain with only secure-headers + retry + rate-limit
- llm-api router now uses llm-chain@file instead of common-chain@file
2026-07-26 19:13:08 +07:00
GitHub Action f0c7534152 chore: update manifests and submodules [skip ci] 2026-07-26 12:09:58 +00:00
asepharyana a4687c9d5b fix(llm-api): update submodule - reasoning/content stream separation 2026-07-26 19:04:36 +07:00
GitHub Action e42e967b7f chore: update manifests and submodules [skip ci] 2026-07-26 11:27:08 +00:00
asepharyana d99bc06df3 fix(llm-api): update submodule to 6ff31b5 — Jinja template via minijinja
- Replaced llama-cpp-2 apply_chat_template with minijinja rendering
- reasoning_content separate from content in API response
- Works with MiniCPM5 thinking model natively
2026-07-26 18:21:43 +07:00
GitHub Action 87df0d7ae2 chore: update manifests and submodules [skip ci] 2026-07-26 10:15:32 +00:00
monrepo-bot 518afa5645 chore: update llm-api to 9a63ff16018f 2026-07-26 10:11:01 +00:00
GitHub Action 2913f515f0 chore: update manifests and submodules [skip ci] 2026-07-26 09:14:25 +00:00
monrepo-bot 05e20df680 chore: update llm-api to 59c77108a55b 2026-07-26 09:09:23 +00:00
GitHub Action 59e2f9d173 chore: update manifests and submodules [skip ci] 2026-07-26 09:08:26 +00:00
monrepo-bot f78b826e59 chore: update llm-api to 495b9ed126e6 2026-07-26 09:03:36 +00:00
GitHub Action d75424634d chore: update manifests and submodules [skip ci] 2026-07-26 08:58:06 +00:00
GitHub Action cc8cefb33a chore: update manifests and submodules [skip ci] 2026-07-26 08:53:27 +00:00
monrepo-bot 757b48b446 chore: update llm-api to 254532458b16 2026-07-26 08:49:23 +00:00
monrepo-bot 20c81b4018 chore: update llm-api to 7c8f747faf43 2026-07-26 08:49:00 +00:00
GitHub Action ca81a8b7ee chore: update manifests and submodules [skip ci] 2026-07-26 08:44:28 +00:00
GitHub Action ac25a51113 chore: update manifests and submodules [skip ci] 2026-07-26 08:42:08 +00:00
asepharyanaandGitHub 10aeda4d4b fix: update llm-api MODEL_PATH for new MiniCPM5 GGUF
Updates MODEL_PATH to MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-Q8_0.gguf
2026-07-26 15:40:14 +07:00
40 changed files with 1255 additions and 819 deletions
+45 -1
View File
@@ -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 -1
View File
@@ -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
+8 -8
View File
@@ -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)
-239
View File
@@ -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
-338
View File
@@ -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
+103
View File
@@ -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)"
+2
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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
+18 -19
View File
@@ -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
+180 -129
View File
@@ -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
+6 -6
View File
@@ -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
+2
View File
@@ -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
```
+2
View File
@@ -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:
```
+2
View File
@@ -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.
+2
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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
}
+209
View File
@@ -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 ];
};
});
}
+124
View File
@@ -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:
+9 -1
View File
@@ -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
+40
View File
@@ -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
+12
View File
@@ -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
+99
View File
@@ -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
+3 -18
View File
@@ -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]
+17
View File
@@ -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
+4
View File
@@ -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
+155 -5
View File
@@ -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'
+28
View File
@@ -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
+28
View File
@@ -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