Skip to content

ci: idempotent autodeploy to netcup host - #26

Merged
ralyodio merged 1 commit into
masterfrom
worktree-mcp-autodeploy
Aug 21, 2026
Merged

ci: idempotent autodeploy to netcup host#26
ralyodio merged 1 commit into
masterfrom
worktree-mcp-autodeploy

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why

mcp.profullstack.com resolves to 104.36.23.197 — the decommissioned iCastCenter VPS (Host profullstack in the SSH config, port 2048). The box is gone: 100% packet loss, 22/80/443/2048 all closed. It had never been migrated: there is no mcp service among the 60+ custom domains in the shared Railway project.

The service is now live on the netcup host (152.53.47.37, profullstack-dev-vienna) at ~/www/mcp-server, running as anthony under systemd behind nginx.

What's here

  • bin/provision.sh — server-side, idempotent. Ensures mise/bun/node/pnpm, pulls the branch, installs deps, writes the systemd unit and nginx vhost, requests TLS when eligible, restarts and health-checks.
  • bin/deploy.sh — local/CI entrypoint. Streams provision.sh over ssh so the logic that runs is always the one from the deploying commit; bootstraps a fresh host by the same path as an update.
  • .github/workflows/deploy.yml — deploys on push to master and on manual dispatch.

Things worth knowing

  • Each mcp_modules/* is its own pnpm project with its own lockfile. A root install does not reach them, and every module with missing deps degrades silently to Could not load metadata at boot. The per-module loop is why this matters.
  • mise's shims are on the login PATH but the mise binary itself is not, so npm's shim dies with mise: command not found under ssh/CI.
  • Runtime is bun. It loads 27/27 modules; node loads 26. csvjson does a bare import { version } from './package.json', which bun supports natively and node rejects without an import attribute.
  • The systemd unit omits ProtectHome (the app lives under /home/anthony/www) and pins an absolute runtime path, since systemd does not run a login shell.
  • TLS issuance is gated on the public A record already resolving to the host, so a run does not burn a Let's Encrypt rate limit while DNS still points at the dead box.
  • The workflow never triggers on pull_request: this repo is public and DEPLOY_SSH_KEY grants shell on the host.

Secrets

DEPLOY_SSH_KEY, DEPLOY_HOST, DEPLOY_USER, DEPLOY_KNOWN_HOSTS are set on the repo; the production environment exists. The key is a dedicated ed25519 pair, already in anthony's authorized_keys.

Still blocking the outage

DNS. mcp.profullstack.com A must move from 104.36.23.197 to 152.53.47.37 at Porkbun. No Porkbun credentials exist in any logicsrc vault (Cloudflare only, which does not serve this domain), so that one step needs a manual change. Re-running the deploy afterwards picks up the certificate automatically.

🤖 Generated with Claude Code

mcp.profullstack.com pointed at 104.36.23.197, the decommissioned iCastCenter
VPS, and had never been migrated anywhere. This adds the deploy path for the
netcup host (152.53.47.37) so the endpoint has a home again.

bin/provision.sh runs on the server and is safe to re-run: every step checks
its desired state first. Two things it handles that a naive deploy misses:

- Each mcp_modules/* is its own pnpm project with its own lockfile, so a root
  install does not reach them. Without the per-module loop, six modules fail
  metadata load at boot and degrade silently.
- mise's shims are on the login PATH but the mise binary is not, so npm's shim
  dies with "mise: command not found" under ssh/CI. PATH is fixed up explicitly.

Runtime is bun: it loads 27/27 modules, where node loads 26 -- csvjson does a
bare `import { version } from './package.json'`, which bun supports natively and
node rejects without an import attribute.

The systemd unit deliberately omits ProtectHome (the app lives under
/home/anthony/www) and pins an absolute runtime path, since systemd does not run
a login shell and would not see the shims.

TLS issuance is gated on the public A record already resolving to this host, so
the run does not burn a Let's Encrypt rate limit while DNS still points at the
dead box. Re-running after the record moves picks the certificate up.

The workflow triggers only on push-to-master and manual dispatch, never on
pull_request: the repo is public and DEPLOY_SSH_KEY grants shell on the host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread bin/deploy.sh
< "$PROVISION"

echo "==> verifying"
if curl -fsS -m 15 --resolve "$DOMAIN:80:$DEPLOY_HOST" "http://$DOMAIN/health" >/dev/null 2>&1; then
Comment thread bin/provision.sh

if ! command -v mise >/dev/null 2>&1; then
log "installing mise"
curl -fsSL https://mise.run | sh
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

19 finding(s)

HIGH/CRITICAL: 16 | MEDIUM: 2 | LOW: 1

Severity Rule Location
HIGH sh-plaintext-download bin/deploy.sh:49
HIGH sh-remote-script-execution bin/provision.sh:43
HIGH secret-generic-api-key mcp_modules/convert2doc/examples/usage-example.js:11
HIGH secret-generic-api-key mcp_modules/fake_json/docs/api.md:151
HIGH secret-generic-api-key mcp_modules/fake_json/README.md:82
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:10
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:22
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:38
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:166
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:187
HIGH secret-generic-api-key mcp_modules/seo-ranking/examples/basic-usage.js:215
HIGH secret-generic-api-key mcp_modules/seo-ranking/README.md:415
HIGH sh-remote-script-execution mcp_modules/wcag/bin/install.sh:111
HIGH sh-remote-script-execution mcp_modules/wcag/bin/install.sh:120
HIGH secret-generic-credential mcp_modules/webhook_verify/examples/basic-usage.js:10
HIGH sensitive-file-committed sample.env:1
MEDIUM manifest-install-lifecycle-script mcp_modules/backlinks/package.json:11
MEDIUM manifest-install-lifecycle-script package.json:16
LOW insecure-temp-file mcp_modules/scanner/test/service.test.js:121

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 6b34d9f into master Aug 21, 2026
9 of 10 checks passed
@ralyodio
ralyodio deleted the worktree-mcp-autodeploy branch August 21, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants