Manual deploys work fine while the team is small. By week three, someone forgets to run the build before pushing the image. By week six, a late-night hotfix goes to the wrong branch. Setting up CI/CD is not a luxury. It is something you should have done on day one.
This post covers a GitHub Actions workflow that builds your Docker image and deploys it to Guara Cloud on every push to the main branch. No paid add-ons, no 400-line YAML files.
Quick answer
You need a .github/workflows/deploy.yml file with four stages: code checkout, Docker image build on GitHub Actions runners, image push to the Guara Cloud registry, and deploy via API. Secrets (API token and registry URL) go into repository settings. The pipeline runs on every push to main, and on pull requests it only runs the build to confirm the image compiles.
Key takeaways
- GitHub Actions provides 2,000 free minutes per month on private repositories. Enough for small teams.
- Build the image on the Actions runner, not on the production server. Guara Cloud receives the finished image.
- Use
docker/build-push-actionwith GitHub cache so subsequent builds finish in seconds. - Keep workflows separate: one to validate (PR) and another to deploy (merge to main).
- Never put API tokens or registry passwords as plain text in YAML. Always use
${{ secrets.NAME }}.
When this flow applies
This setup works well for web applications (Node.js, Next.js, NestJS, Python, Go) that run in containers and use GitHub as the source. If the team has 1 to 10 developers and ships daily or weekly, Actions automation covers the entire use case. It works for both staging and production if you use different branches.
When not to use this flow
If the repository is very large (2GB+ monorepo), checkout time on Actions starts to hurt. Sparse checkout or local CI might be better in that case. If the company requires self-hosted runners for compliance, the YAML barely changes but the infrastructure changes significantly. For projects that do not use containers (static file deploys, for example), the flow is different and simpler.
Before you start
- GitHub repository with your application code
- Working Dockerfile at the project root
- Active Guara Cloud account with a service already created
- Guara Cloud API token (generated in the settings panel)
- Admin permission on the repository to add secrets
1. Create the API token and configure secrets
In Guara Cloud, go to Settings > API Tokens and generate a token with deploy scope. Copy the token. In GitHub, open the repository, go to Settings > Secrets and variables > Actions, and add:
Required secrets in GitHub
| Secret | Description |
|---|---|
GUARA_API_TOKEN | API token generated in the Guara Cloud panel |
GUARA_REGISTRY_URL | Container registry URL (e.g. registry.guaracloud.com) |
GUARA_SERVICE_ID | ID of the service to deploy to |
Do not share these values in issues, PRs, or workflow logs. GitHub automatically masks secret values in logs, but being careful never hurts.
2. The validation workflow (for pull requests)
This file runs on every PR. It builds the Docker image to confirm the Dockerfile still works, but does not push or deploy anything.
name: Validate
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: test-build:pr-${{ github.event.pull_request.number }}
cache-from: type=gha
cache-to: type=gha,mode=max If the build fails here, the PR shows red and nobody can merge. This prevents broken images from reaching main.
3. The deploy workflow (for merge to main)
This is the file that does the actual work. It runs when something lands on main, typically through a merged and approved PR.
name: Deploy
on:
push:
branches: [main]
env:
IMAGE_TAG: ${{ github.sha }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Guara Cloud registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.GUARA_REGISTRY_URL }}
username: api
password: *** secrets.GUARA_API_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.GUARA_REGISTRY_URL }}/${{ secrets.GUARA_SERVICE_ID }}:${{ env.IMAGE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to Guara Cloud
run: |
curl -X POST https://api.guaracloud.com/v1/services/${{ secrets.GUARA_SERVICE_ID }}/deploy \
-H "Authorization: Bearer *** secrets.GUARA_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"image_tag": "'${{ env.IMAGE_TAG }}'"}'
shell: bash Every deploy uses the commit SHA as the image tag. This makes rollback straightforward later, because the tag maps exactly to the commit that was deployed.
4. Cut build time with caching
Without cache, docker build reinstalls every dependency on each pipeline run. With GitHub Actions cache, unchanged layers are reused.
The difference is noticeable. On a typical Node.js project, a build without cache takes 3 to 4 minutes. With cache enabled, it drops to 40 to 60 seconds when only application code changed.
The lines responsible for this in the YAML above are:
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max exports all layers, not just the final stage. In multi-stage Dockerfiles, this matters because the build stage (which runs npm install) also gets cached.
5. Deploy notifications
Watching a pipeline fail without knowing about it is bad. Add a Slack or Discord notification as the last step in the workflow:
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v2
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Deploy ${{ job.status }}: ${{ github.repository }}@${{ github.sha }}"
} The if: always() ensures the notification fires even when the deploy fails. Without it, you only find out by checking GitHub manually.
6. Pre-merge checks
The validation workflow (step 2) can be extended. Adding tests and lint to the same job ensures the PR does not merge with failures:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
cache-from: type=gha
cache-to: type=gha,mode=max Set up a branch protection rule on main to require the validate job to pass before allowing merges. This stops broken code from reaching production.
Common problems
- Problem Docker login fails with 401 Unauthorized
- How to fix Check that the API token has deploy scope and the GUARA_API_TOKEN secret is configured correctly. Tokens expire, so generate a fresh one if needed.
- Problem Image build takes more than 10 minutes
- How to fix Enable GHA cache in build-push-action. If already enabled, verify that the Dockerfile orders layers correctly: dependencies first, source code last.
- Problem Deploy triggers on push to any branch
- How to fix Confirm the on.push.branches trigger lists only [main]. Patterns like branches: ["*"] fire on every branch.
- Problem Deploy curl returns 404
- How to fix The GUARA_SERVICE_ID might be wrong. Check the Guara Cloud panel and confirm the service exists in your account.
- Problem Logs show "layer not found" in cache
- How to fix GHA cache has a size limit. If the project produces many layers, consider using registry cache (type=registry) instead of GHA.
Can I use this flow with monorepos (Nx, Turborepo)?
Yes, but adjust the build context in build-push-action to point to the specific application directory. Also use path filters in the trigger so the workflow only runs when files for that application change.
How much do GitHub Actions minutes cost?
Public repositories get unlimited minutes. Private repos include 2,000 minutes per month on the Free plan, 3,000 on Pro, and 10,000 on Team. A 5-minute build running 20 deploys per month consumes 100 minutes.
Do I pay extra on Guara Cloud for CI/CD?
No. The registry and deploy API are part of your plan. You only pay for container execution time, same as manual deploys.
How do I roll back if an automated deploy broke production?
Each deploy uses the commit SHA as the image tag. In the Guara Cloud panel, select the previous version and click Rollback. Or revert the commit on main and the pipeline will automatically deploy the corrected version.
Can I have staging and production environments in the same pipeline?
Yes. Add a separate staging job that runs first, and make the production job depend on it with needs: [staging]. Use different branches (develop for staging, main for production).
What to do next
With the pipeline running, the typical next steps are adding integration tests to the validation stage and setting up preview deployments for each PR. Guara Cloud supports ephemeral services that you can spin up and tear down automatically.
Deploy your application in Brazil
Containers with HTTPS, public domain, and billing in BRL. No surprises on your invoice.