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
This commit is contained in:
Asep Haryana
2026-07-30 19:45:43 +07:00
parent 46730ec07d
commit b5596e1398
4 changed files with 107 additions and 33 deletions
+52 -17
View File
@@ -1,4 +1,4 @@
name: Nix Build & Deploy name: Nix Build & Deploy — All Services
on: on:
push: push:
@@ -18,16 +18,20 @@ concurrency:
permissions: permissions:
contents: read contents: read
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
jobs: jobs:
build-and-deploy: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
service: [hub, scraper] service: [hub, scraper, tools-gateway, tools-workers, tools-frontend, llm-api]
steps: steps:
- name: Checkout with submodules - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
@@ -40,32 +44,63 @@ jobs:
sandbox = false sandbox = false
accept-flake-config = true accept-flake-config = true
- name: Use Nix cache - name: Cache Nix
uses: DeterminateSystems/magic-nix-cache-action@v8 uses: DeterminateSystems/magic-nix-cache-action@v8
- name: Build ${{ matrix.service }} with Nix - name: Build ${{ matrix.service }}
run: nix build .#${{ matrix.service }} --impure --option sandbox false run: nix build .#${{ matrix.service }} --impure --option sandbox false
- name: Nix copy via SSH - name: Nix copy to VPS
env: env:
SSH_KEY: ${{ secrets.VPS_SSH_KEY }} SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_ed25519 echo "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
nix copy --to "ssh://$VPS_USER@$VPS_HOST" ./result nix copy --to "ssh://$VPS_USER@$VPS_HOST" ./result
- name: Deploy ${{ matrix.service }} on VPS deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16
- name: Deploy all services on VPS
env: env:
VPS_HOST: ${{ secrets.VPS_HOST }} SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_USER: ${{ secrets.VPS_USER }} 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
for service in hub scraper tools-gateway tools-workers tools-frontend llm-api; do
echo "=== Deploying $service ==="
ssh "$VPS_USER@$VPS_HOST" "
PROFILE=\"/nix/var/nix/profiles/\$service\"
CURRENT=\$(readlink -f \"\$PROFILE\" 2>/dev/null || echo "")
LATEST=\$(ls -1d /nix/store/*-\$service-* 2>/dev/null | tail -1)
if [ -n \"\$LATEST\" ] && [ \"\$CURRENT\" != \"\$LATEST\" ]; then
sudo /nix/var/nix/profiles/default/bin/nix-env --profile \"\$PROFILE\" --set \"\$LATEST\" &&
sudo systemctl restart \"\$service\" &&
echo \" ✅ \$service updated to \$LATEST\"
elif [ -z \"\$LATEST\" ]; then
echo \" ⚠️ \$service: no store path found\"
else
echo \" \$service: already up-to-date\"
fi
"
done
- name: Verify services
env:
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
run: | run: |
STORE_PATH=$(readlink result)
PROFILE_NAME="${{ matrix.service }}"
ssh "$VPS_USER@$VPS_HOST" " ssh "$VPS_USER@$VPS_HOST" "
sudo /nix/var/nix/profiles/default/bin/nix-env --profile /nix/var/nix/profiles/$PROFILE_NAME --set '$STORE_PATH' && for service in hub scraper tools-gateway tools-workers tools-frontend llm-api; do
sudo systemctl restart $PROFILE_NAME state=\$(systemctl is-active \$service 2>/dev/null || echo 'not-found')
echo \" \$service: \$state\"
done
" "
+25 -14
View File
@@ -20,22 +20,25 @@
inherit name src; inherit name src;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
cacert curl gcc gnumake openssl pkg-config python3 cacert curl gcc gnumake openssl pkg-config python3 libclang
] ++ nativeBuildInputs; ] ++ nativeBuildInputs;
buildInputs = with pkgs; [ buildInputs = with pkgs; [
nodejs openssl nodejs openssl
] ++ buildInputs; ] ++ buildInputs;
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
LD_LIBRARY_PATH = "${pkgs.libclang.lib}/lib:${pkgs.stdenv.cc.cc.lib}/lib";
SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
NODE_EXTRA_CA_CERTS = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; NODE_EXTRA_CA_CERTS = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
NODE_ENV = "production"; NODE_ENV = "production";
HOME = "/tmp";
CARGO_HOME = "/tmp/.cargo";
phases = [ "unpackPhase" "buildPhase" "installPhase" ]; phases = [ "unpackPhase" "buildPhase" "installPhase" ];
buildPhase = buildScript; buildPhase = ''
export HOME="$TMPDIR" CARGO_HOME="$TMPDIR/.cargo-${name}"
SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
'' + buildScript;
installPhase = installScript; installPhase = installScript;
}; };
@@ -60,7 +63,7 @@
rev = if name == "hub" then "b007ced615dcc78392fc938350a23e8f022d83b7" rev = if name == "hub" then "b007ced615dcc78392fc938350a23e8f022d83b7"
else if name == "scraper" then "bc782ae4f825892f2d28901f4a5527f353106db1" else if name == "scraper" then "bc782ae4f825892f2d28901f4a5527f353106db1"
else if name == "tools" then "3956b90c3ce39ffa7ffba8084937f20e11364d6b" else if name == "tools" then "3956b90c3ce39ffa7ffba8084937f20e11364d6b"
else if name == "llm-api" then "HEAD" else if name == "llm-api" then "67861f384bd4f64d5236a9608b33c965fbebbc7f"
else "HEAD"; else "HEAD";
submodules = true; submodules = true;
}; };
@@ -122,14 +125,14 @@ WRAPPER
installScript = '' installScript = ''
mkdir -p $out/bin mkdir -p $out/bin
cp backend/target/release/tools-gateway $out/bin/tools-gateway cp target/release/tools-gateway $out/bin/tools-gateway
''; '';
}; };
tools-workers = mkApp { tools-workers = mkApp {
name = "tools-workers-0.1.0"; name = "tools-workers-0.1.0";
src = submoduleSrc "tools"; src = submoduleSrc "tools";
nativeBuildInputs = cargoDeps ++ [ pkgs.tesseract ]; nativeBuildInputs = cargoDeps ++ [ pkgs.tesseract pkgs.leptonica ];
buildScript = '' buildScript = ''
cd backend cd backend
@@ -138,8 +141,9 @@ WRAPPER
''; '';
installScript = '' installScript = ''
cd backend
mkdir -p $out/bin mkdir -p $out/bin
cp backend/target/release/tools-workers $out/bin/tools-workers cp target/release/tools-workers $out/bin/tools-workers
''; '';
}; };
@@ -158,10 +162,10 @@ WRAPPER
installScript = '' installScript = ''
mkdir -p $out/share/tools-frontend $out/bin mkdir -p $out/share/tools-frontend $out/bin
cp -r frontend/.next $out/share/tools-frontend/ cp -r .next $out/share/tools-frontend/
cp -r frontend/public $out/share/tools-frontend/ 2>/dev/null || true cp -r public $out/share/tools-frontend/ 2>/dev/null || true
cp frontend/package.json $out/share/tools-frontend/ cp package.json $out/share/tools-frontend/
cp -r frontend/node_modules $out/share/tools-frontend/ cp -r node_modules $out/share/tools-frontend/
cat > $out/bin/tools-frontend << WRAPPER cat > $out/bin/tools-frontend << WRAPPER
#!${pkgs.runtimeShell} #!${pkgs.runtimeShell}
exec ${pkgs.bun}/bin/bun run --cwd $out/share/tools-frontend start exec ${pkgs.bun}/bin/bun run --cwd $out/share/tools-frontend start
@@ -173,10 +177,17 @@ WRAPPER
llm-api = mkApp { llm-api = mkApp {
name = "llm-api-0.1.0"; name = "llm-api-0.1.0";
src = submoduleSrc "llm-api"; src = submoduleSrc "llm-api";
nativeBuildInputs = cargoDeps; nativeBuildInputs = cargoDeps ++ [ pkgs.cmake pkgs.gcc ];
buildScript = '' buildScript = ''
echo "=== Building llm-api ===" echo "=== Building llm-api ==="
export HOME="$NIX_BUILD_TOP"
export CARGO_HOME="$NIX_BUILD_TOP/.cargo"
export NIX_CFLAGS_COMPILE="-idirafter ${pkgs.stdenv.cc.cc}/include/c++/${pkgs.stdenv.cc.cc.version} -idirafter ${pkgs.stdenv.cc.cc}/include/c++/${pkgs.stdenv.cc.cc.version}/x86_64-unknown-linux-gnu"
export NIX_LDFLAGS="-L${pkgs.stdenv.cc.cc.lib}/lib -lstdc++"
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.libffi}/lib:$LD_LIBRARY_PATH"
export LLVM_CONFIG_PATH="${pkgs.clang}/bin/llvm-config"
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib"
cargo build --release 2>&1 cargo build --release 2>&1
''; '';
+2 -2
View File
@@ -64,12 +64,12 @@ http:
tools-service: tools-service:
loadBalancer: loadBalancer:
servers: servers:
- url: 'http://tools:3000' - url: 'http://host.docker.internal:3500'
llm-api-service: llm-api-service:
loadBalancer: loadBalancer:
servers: servers:
- url: 'http://llm-api:8080' - url: 'http://host.docker.internal:8080'
jaeger-service: jaeger-service:
loadBalancer: loadBalancer:
+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