feat(deploy): production deployment scripts and CD pipeline (Closes #469) - #486
Merged
Nathydre merged 1 commit intoAug 28, 2026
Merged
Conversation
Closes nathydre21#469 A complete, reviewable production deployment system for NEPA: - Dockerfiles for all three services: backend (node, multi-stage, non-root, tini, /health healthcheck), frontend (vite build -> nginx SPA with /api + /socket.io reverse proxy), and the Soroban contract (rust wasm build + artifact verification). - docker-compose.prod.yml: backend + frontend + postgres + redis with healthchecks, resource limits, named volumes, and an internal network; only the frontend edge port is published. - .github/workflows/cd.yml: PRs lint the artifacts (hadolint, shellcheck, compose config, actionlint); pushes build & push images to GHCR and deploy to staging (testnet) on main / production (mainnet) on v*.*.* tags; workflow_dispatch drives manual deploy and rollback. Host deploys are gated on *_SSH_HOST secrets and fall back to a validated dry-run so the pipeline is green before any infrastructure exists. - Rolling deployment with a health gate and automatic rollback (deploy/deploy.sh), plus a manual rollback path (deploy/rollback.sh). - Environment-specific config templates for testnet and mainnet (deploy/.env.{staging,production}.example). - Deployment notifications to Slack and/or a generic webhook (deploy/notify.sh); no-op when unconfigured. - docs/DEPLOYMENT.md operational runbook (architecture, images, envs, rolling+rollback strategy, secrets, first-time setup, troubleshooting). - Fix: add the missing DATABASE_URL to backend/.env.example, required by the root Prisma schema (blocks a clean deploy without it).
Comment on lines
+112
to
+122
| run: | | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.image_tag }}" ]; then | ||
| tag="${{ github.event.inputs.image_tag }}" | ||
| elif [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then | ||
| tag="${GITHUB_REF_NAME}" | ||
| else | ||
| tag="sha-${GITHUB_SHA::12}" | ||
| fi | ||
| echo "image_tag=${tag}" >> "$GITHUB_OUTPUT" | ||
| echo "Resolved image tag: ${tag}" | ||
|
|
| proxy_http_version 1.1; | ||
| proxy_set_header Upgrade $http_upgrade; | ||
| proxy_set_header Connection "upgrade"; | ||
| proxy_set_header Host $host; |
Comment on lines
+57
to
+59
| proxy_http_version 1.1; | ||
| proxy_set_header Upgrade $http_upgrade; | ||
| proxy_set_header Connection "upgrade"; |
| location /api/ { | ||
| proxy_pass http://backend:3001; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; |
| location = /health { | ||
| proxy_pass http://backend:3001/health; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; |
| run: echo "name=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Build and push | ||
| uses: docker/build-push-action@v6 |
| - uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3 |
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: docker/setup-buildx-action@v3 |
| matrix: | ||
| service: [backend, frontend] | ||
| steps: | ||
| - uses: actions/checkout@v4 |
| name: Validate deployment artifacts | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #469.
Adds a complete, production-grade deployment system for NEPA — container images for every service, a production Compose stack, a full CD pipeline, a health-gated rolling deploy with automatic rollback, testnet/mainnet config, and deploy notifications.
The design goal was a PR that is correct and green on day one, with zero infrastructure provisioned, yet flips to real deployments the moment secrets are added — no workflow edits required.
Acceptance criteria
backend/Dockerfile,frontend/Dockerfile,contract/Dockerfiledocker-compose.prod.yml.github/workflows/cd.ymldeploy/deploy.sh(blue-green documented as the scale-out path)deploy/deploy.sh; manualdeploy/rollback.sh+workflow_dispatchdeploy/.env.staging.example(testnet),deploy/.env.production.example(mainnet)deploy/notify.sh(Slack + generic webhook), called by every deploy/rollback jobImages
All multi-stage; compilers and dev deps never reach the runtime layer.
node:20-slim, prod-only deps, Prisma client generated in the builder stage, runs as a non-root user undertini, healthcheck on/health.slim(glibc) is deliberate: the backend has native addons that don't build cleanly on Alpine/musl.nginx:1.27-alpineserving the SPA and reverse-proxying/api+/socket.io(WebSocket upgrade) to the backend./healthproxies the backend for a true end-to-end gate;/healthzis nginx-local.wasm32-unknown-unknownbuild that verifies the Soroban.wasm. Defaults tobuild(verify, no network); on-chaindeployis opt-in and gated onSTELLAR_SECRET_KEY.CD pipeline (
.github/workflows/cd.yml)docker compose config, and actionlint. No build/push.main→ build + push images to GHCR → deploy staging (Stellar testnet).v*.*.*tag → build all images (incl. contract) → deploy production (Stellar mainnet).workflow_dispatch→ manual deploy or rollback of a chosen environment/tag.Host deploys run over SSH and are gated on the environment's
*_SSH_HOSTsecret. With no infrastructure configured, deploy jobs fall back to a validated dry-run (compose config+ the exact command that would run), so the pipeline is green and reviewable before any server exists. Concurrency is serialized per environment withcancel-in-progress: falseso a deploy is never interrupted mid-roll.Deployment strategy: rolling + automatic rollback
deploy/deploy.sh:nepa.image.tagcontainer label).docker compose up -d --wait(blocks on container healthchecks).backend/scripts/verify-deployment.shagainst the end-to-end/healthendpoint.0healthy,1rolled back (release rejected, service restored),2rollback also failed (page a human).A bad image never survives the health gate, so a failed release is self-healing. Blue-green is documented in
docs/DEPLOYMENT.mdas the natural scale-out (two stacks behind the TLS proxy, flip after the gate) reusing the same bookkeeping.Validation performed
bash -non all four shell scripts — clean.actionlinton the workflow — clean.compose configalso run in the PR's ownvalidatejob.Enabling real deploys (no code changes)
Add per-environment secrets —
STAGING_SSH_HOST/_USER/_KEY(+STAGING_DEPLOY_PATH), thePRODUCTION_*equivalents, optionalSTELLAR_SECRET_KEY, andSLACK_WEBHOOK_URL/WEBHOOK_ALERT_URL. First-time host setup, the secrets table, and troubleshooting are indocs/DEPLOYMENT.md.Note
backend/.env.examplegains the missingDATABASE_URLrequired by the root Prisma schema (prisma generate/migrateand the server fail without it) — a small fix that unblocks a clean deploy.