Originally published on the Hubfly space blog.
TL;DR
- What you get: every push to
mainredeploys your container on Hubfly space, and you can send a rollback action when a release goes bad.- Who it's for: developers who build images or test code in GitHub Actions (or GitLab CI) and still redeploy by hand.
- Time: about 15 minutes.
- Cost: deploy webhooks are free. You pay only for the container itself; the shared tier is free for 360 hours a month.
Your app is live, but every change still ends with you opening the dashboard and clicking Redeploy. That's slow, and it's how the wrong build reaches your users on a Friday evening. In this guide you'll connect GitHub Actions to Hubfly space with a signed deploy webhook, so every push to main goes live on its own once your tests pass, and you can send a rollback when a release goes bad.
Try it on Hubfly space. Start free, 360 hours included →
What a deploy webhook does
A deploy webhook is an address your CI calls when a build is ready. Hubfly space then does one action on your container: pull the new image or commit, update its settings, restart it, or roll it back. Pushing an image to a registry never redeploys anything by itself, so the webhook is the missing "now ship it" step.
Each webhook has three settings that matter:
| Setting | What it controls | Pick |
|---|---|---|
| Security mode | How Hubfly space checks the caller | HMAC-SHA256 for anything real |
| Allowed actions | What the caller may do | Only what your pipeline needs |
| IP allowlist | Where calls may come from | Your fixed runner IPs, if you have them |
The three security modes differ a lot:
| Mode | How the call is checked | Actions the dashboard offers |
|---|---|---|
| Basic | Token inside the URL | Image Pull only |
| Standard | Token inside the URL | Image Pull, Provisioning, Hard Restart |
| HMAC-SHA256 (secure) | Signed timestamp + body, no token in the URL | Every action, including Config Update and Rollback Recovery |
Why it matters: with Basic or Standard, anyone who sees the URL (a leaked build log is enough) can trigger it. With HMAC-SHA256 the URL alone is useless. Each request must be signed with a secret, and the signature expires after five minutes, so a captured request can't be replayed later.
Step 1: Create the webhook
Open your project, then the Pipeline tab, and click Create webhook.
- Give it a clear Webhook name, such as
GitHub Actions deploy, and an optional description. - Under Target Container Context, pick the container this webhook may deploy. Leave it on All Containers (Project Scope) only if one pipeline deploys several services; then every request must say which container it means.
Click Next Step.
Choose HMAC-SHA256.
Under Allowed Operation Permissions, turn on Image Pull and Rollback Recovery. Add Config Update only if your pipeline also changes environment variables. Leave Deprovisioning off: a deploy pipeline never needs to delete anything.
If your CI runs on a machine with a fixed public IP (a self-hosted runner, a build server), enter it under IP Allowlist Restriction.
Click Register Webhook.
A dialog then shows the HMAC Signing Secret Key and the Deployment Pipeline URL Target. The secret is shown once. Copy both now; if you lose the secret, delete the webhook and create a new one. Webhooks can't be edited after you create them, so changing the allowed actions also means creating a new one.
Step 2: Store the values in GitHub
In your repository, open Settings → Secrets and variables → Actions and add two repository secrets:
-
HUBFLY_WEBHOOK_URL: the URL you copied (it looks likehttps://api.hubfly.space/api/webhooks/deploy/whk_…). -
HUBFLY_WEBHOOK_SECRET: the signing secret.
Secrets are masked in logs, which keeps the signing key out of your build output.
Step 3: Sign and send the request
Add this workflow as .github/workflows/deploy.yml. The deploy job waits for test, so a failed test never reaches production. Replace the test step with your own.
name: Deploy to Hubfly space
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "run your tests here"
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Trigger Hubfly space deployment
env:
HUBFLY_WEBHOOK_URL: ${{ secrets.HUBFLY_WEBHOOK_URL }}
HUBFLY_WEBHOOK_SECRET: ${{ secrets.HUBFLY_WEBHOOK_SECRET }}
COMMIT_SHA: ${{ github.sha }}
run: |
body=$(printf '{"action":"pull","ref":"main","commitSha":"%s"}' "$COMMIT_SHA")
timestamp=$(date +%s)
signature=$(printf '%s.%s' "$timestamp" "$body" \
| openssl dgst -sha256 -hmac "$HUBFLY_WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl --fail-with-body -X POST "$HUBFLY_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-H "X-Hub-Timestamp: $timestamp" \
-H "X-Hub-Signature-256: sha256=$signature" \
--data-binary "$body"
What each line does, in plain words:
-
bodyis the exact JSON you send. For a Git-backed container it names the branch and commit. For an image-based container, send{"action":"pull","image":"ghcr.io/your-org/your-app:sha-7f3b1a9"}instead. -
timestampis the current Unix time. It's part of the signature, which is what stops old requests from being replayed. -
signatureis an HMAC-SHA256 oftimestamp.body, with the dot. Hubfly space computes the same value with your secret and compares. -
--data-binarysends the body byte for byte. Change a single space after signing and the signature no longer matches. -
--fail-with-bodymakes the job fail, and print the reason, if Hubfly space refuses the call.
On a self-hosted runner, keep in mind that openssl -hmac puts the secret on the command line, where other users on that machine could see it in the process list. Use a dedicated runner user.
For a project-scoped webhook, add "containerId":"<your container ID>" to the body.
Deploy an immutable tag (sha-7f3b1a9) or a commit SHA rather than latest. Then you always know which build is running, and a rollback has something specific to go back to.
Step 4: Watch it, and roll back when needed
Each call shows up under Pipeline → Recent deployments with its action, trigger, status, duration and any error. The webhook card counts Triggers and shows the Last Run. You can also list the history with GET /api/v1/projects/<project-id>/deployments.
If a release is bad, send the rollback action with the same signing step:
body='{"action":"rollback"}'
Rollback is an explicit action: send it yourself, from a manual workflow or an incident script. When you send an update, the docs also list rollback options for it: rollback.enabled, rollback.onHealthCheckFail and a positive rollback.timeoutSeconds, meant to revert the update if the new version fails its health check. A rollback needs a previous version to go back to, and a health-check trigger needs a health check, so always open the deployment event to confirm what actually happened.
What can go wrong
| You see | Why | Fix |
|---|---|---|
401 |
Wrong or missing URL token (Basic/Standard) | Copy the full URL again. Never add a token to a secure URL. |
400 on a secure webhook |
Bad signature, a timestamp older than five minutes, or the body changed after signing | Sign timestamp.body and send the same bytes with --data-binary. Check the runner's clock. |
403 |
The action isn't allowed, or the caller's IP isn't on the allowlist | Create a new webhook with the right actions or IPs. |
| "Container is missing" | Project-scoped webhook called without containerId
|
Add containerId to the body. |
| Image pushed, nothing changed | A registry push never redeploys by itself | Call the webhook with {"action":"pull"}. |
Limits and pricing
- Webhooks are free. There's no charge per webhook or per call.
- The container is what you pay for. The shared tier (0.3 vCPU, 256 MB) is free for 360 runtime hours a month and sleeps when idle. A dedicated container with 1 vCPU and 512 MB running all month costs 1 × $0.004 × 730 + 0.5 × $0.002 × 730 = $2.92 + $0.73 = $3.65 a month.
- Five-minute window for secure signatures.
- One action per request, and only the actions you allowed when you created the webhook.
- No edits: to change a webhook's mode, actions or IPs, create a new one and delete the old one.
Who this is not for
- Teams on GitHub-hosted runners who want an IP allowlist. Those runners don't have fixed addresses, so the allowlist only fits self-hosted runners or a fixed-IP build server. Use the signature alone, or move the deploy job to a self-hosted runner.
- Anyone who wants Hubfly space to build and deploy on every push with no CI at all. Connect the repo directly instead: Git deploys can deploy on push, or after CI succeeds.
-
Teams that want automatic rollback without health checks. Automatic rollback on an update depends on a health check (
rollback.onHealthCheckFail). Add one first. - Pipelines that need preview environments per pull request. A webhook deploys to existing containers; it doesn't create a new environment for each branch.
Read next
- Docker health checks that catch real problems, explained simply
- Canary releases with a weighted load balancer
- Gate your app on a database migration job
Go further on Hubfly space
- Add a health check so a failing release can be caught and rolled back: see the observability docs.
- Send 10% of traffic to the new version first with a weighted load balancer: load balancer docs.
- Get notified when an image is pushed or scanned with registry webhooks: webhooks docs.
Ready to stop redeploying by hand? No card needed: Start free, 360 hours included →
Top comments (0)