GitHub Actions CI/CD Deploy Design
Context
HMP IoT Platform currently deploys through release.sh. The script builds backend and frontend Docker images for linux/amd64, pushes them to GHCR, connects to the VPS over SSH, uploads production compose/env files, runs migrations, starts services, and verifies health.
The new workflow keeps the same production model:
- GitHub Actions builds and publishes images to GHCR.
- GitHub Actions deploys to the existing VPS over SSH.
- Runtime app secrets stay on the VPS in
.env.production. - Pull requests targeting
masterrun lint and build checks; closing a PR as merged intomasterautomatically deploys production.
Goals
- Add GitHub Actions CI/CD for production deploy after PR merge to
master. - Gate deploy with backend/frontend Biome lint plus backend/frontend builds.
- Publish backend and frontend images to GHCR.
- Deploy immutable image tags to the VPS over SSH.
- Reuse the existing Docker Compose production stack and remote migration/health flow.
- Avoid storing database, JWT, SMTP, MQTT, or MinIO runtime secrets in GitHub.
Non-Goals
- No migration away from VPS hosting.
- No change to
docker-compose.prod.ymlservice topology unless required by the workflow. - No automatic rollback in the first version.
- No backend Jest test gate in this first workflow.
- No removal of
release.sh; it remains a manual fallback.
Workflow Shape
Create .github/workflows/deploy.yml.
Triggers:
pull_requesttomaster.
Permissions:
contents: readpackages: write
Concurrency:
- One workflow run per pull request ref at a time.
- Newer PR updates cancel older queued checks for the same PR.
Jobs:
quality- Checkout repo.
- Install Bun with
oven-sh/setup-bun. - Run
bun install --frozen-lockfile. - Run
bun run backend:lint. - Run
bun run frontend:lint. - Run
bun run backend:build. - Run
bun run frontend:build. - Runs on pull request open, update, reopen, ready-for-review, and close events.
publish- Depends on
quality. - Runs only when the pull request event action is
closedandpull_request.mergedistrue. - Login to GHCR with
GITHUB_TOKEN. - Build and push backend image from
docker/backend.Dockerfile. - Build and push frontend image from
docker/frontend.Dockerfile. - Target platform:
linux/amd64. - Tags:
${{ github.sha }}latest
- Depends on
deploy- Depends on
publish. - Runs only when the pull request event action is
closedandpull_request.mergedistrue. - Configure SSH from GitHub Actions secret.
- Create remote app directory and
backupsdirectory if missing. - Upload
docker-compose.prod.yml. - Run remote deployment commands on the VPS.
- Depends on
Image Naming
Use GHCR images under the current repository owner:
- Backend:
ghcr.io/<owner>/iot_platform-backend:<tag> - Frontend:
ghcr.io/<owner>/iot_platform-frontend:<tag>
The deployed tag should be the immutable PR merge commit SHA. latest may also be pushed for human convenience, but the VPS deploy should use the SHA tag to avoid ambiguity.
Required GitHub Secrets
Transport and build-time values:
VPS_HOSTVPS_USERVPS_SSH_PRIVATE_KEYREMOTE_APP_DIRVITE_API_URLVITE_WS_URLVITE_APP_NAME
GitHub should not store .env.production runtime secrets for this design.
The workflow uses fixed ports: SSH 22, backend host 3000, and frontend host 5173.
VPS Requirements
The VPS must already have:
- Docker and Docker Compose plugin installed.
- A readable
.env.productionatREMOTE_APP_DIR/.env.production. - Required runtime env keys in
.env.production:JWT_SECRETPOSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBDATABASE_URL
- Permission to pull GHCR images.
If images are private and the VPS cannot pull them anonymously, add a later enhancement for remote GHCR login. The first version assumes the VPS can pull the published images.
Remote Deploy Flow
The remote script should:
cdintoREMOTE_APP_DIR.- Create
backups. - Confirm
.env.productionexists. - Validate required env keys.
- Export:
BACKEND_IMAGEFRONTEND_IMAGEBACKEND_HOST_PORT=3000FRONTEND_HOST_PORT=5173
- Run
docker compose -f docker-compose.prod.yml --env-file .env.production config. - Pull backend/frontend images.
- Start infra services:
postgresredisemqxminioportainer
- Wait for
hmpiot_postgreshealth to becomehealthy. - Run backend migrations with
docker compose run --rm backend bun run migration:run. - Start backend/frontend.
- Verify backend/frontend containers are running.
- Verify backend/frontend health status is
healthyor has no health check. - Print
docker compose ps.
On failure, print docker compose ps -a and the last backend/frontend logs.
Failure Behavior
- Backend/frontend lint or build failure stops before publish and deploy.
- Docker build/push failure stops before deploy.
- Remote deploy failure exits non-zero and surfaces container status/logs.
- No automatic rollback in first version. Existing Docker Compose state and logs remain available on the VPS for manual recovery.
Documentation Updates
Update:
README.mdrelease section: production deploy now happens on merged PR close events formaster.docs/deployment-guide.md: document GitHub secrets, VPS.env.productionownership, and deploy sequence.
Keep:
release.shas local/manual fallback.- Existing
.vps.config.exampleunless a later change adds a GitHub Actions-specific example.
Verification Plan
Before merging workflow implementation:
- Run
bun run backend:lint. - Run
bun run frontend:lint. - Run
bun run backend:build. - Run
bun run frontend:build. - Validate workflow syntax by reviewing
.github/workflows/deploy.yml. - Confirm required GitHub secret names are documented.
After merge:
- Watch first PR merge deployment run in GitHub Actions.
- Confirm GHCR contains backend/frontend SHA tags.
- Confirm VPS containers use the SHA-tagged images.
- Confirm backend health endpoint and frontend page respond through the existing reverse proxy.
Unresolved Questions
None.