Skip to content

Recipe: Enhance an existing GitHub workflow with Reoclo route sync

Use this recipe when:

  • A .github/workflows/*.yml file exists in the repository.
  • At least one workflow step contains docker compose, docker run, or docker stack.
  • The user has a Reoclo account and at least one Application with a linked_container_name configured.

Do not use this recipe if the repository has no existing Docker workflow. Use Set Up External Deployment instead.

Before making any changes, confirm the recipe applies.

Step 1 — Find workflow files with Docker commands:

Terminal window
grep -rl "docker" .github/workflows/

For each matched file, check for docker compose up, docker compose up -d, docker run, or docker stack deploy.

Step 2 — Confirm a Reoclo Application matches the container (and the key has scope):

There is no public endpoint that lists Applications, and no key-introspection endpoint, for an automation key. Probe the deploy-session endpoint instead — it returns the Applications whose linked_container_name matched (applications) and the names that did not (unmatched), and it returns 403 when the key lacks external_deploy scope. Read both the body and the HTTP status:

Terminal window
curl -s -w '\n%{http_code}\n' -X POST https://api.reoclo.com/external-deploy/session \
-H "Authorization: Bearer $REOCLO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"container_names": ["YOUR_CONTAINER_NAME"]}'
  • A trailing 201 with your container under applications → the recipe can proceed.
  • Your container under unmatched (or an empty applications) → no Application has a matching linked_container_name. Stop and tell the user a Reoclo Application linked to this container must exist first.
  • A trailing 403 → continue to Step 3.

Step 3 — If Step 2 returned 403, add the external_deploy scope.

The 403 from the Step 2 probe is the signal that the rca_ key is missing external_deploy. Instruct the user to add it to the key’s allowed operations in the dashboard at https://app.reoclo.com under API Keys > Automation Keys, then re-run Step 2. A 201 confirms the scope is present.

Insert a reoclo/deploy-sync@v2 step immediately after the docker compose up / docker run step in each qualifying workflow job.

Before:

- name: Deploy containers
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: |
cd /srv/reoclo/myapp
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
timeout: 300

After:

- name: Deploy containers
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: |
cd /srv/reoclo/myapp
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
timeout: 300
- name: Sync Reoclo proxy routes
uses: reoclo/deploy-sync@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
compose_file: ./docker-compose.prod.yml

Rules for placing the step:

  • Always insert after the last docker compose up / docker run step in the job, not before it.
  • If the workflow uses a matrix strategy, add the step inside the matrix job so it runs once per matrix entry.
  • If the job has a needs: dependency on a build job, place the sync step in the deploy job, not the build job.
  • Do not add the step to jobs that only build images without deploying them.

If the workflow does not use a Compose file (bare docker run commands), use the services input instead:

- name: Sync Reoclo proxy routes
uses: reoclo/deploy-sync@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
services: "myapp-web:3000"

Determine the correct name:port value by reading the container name and port from the docker run command in the workflow.

After editing the workflow file, run these checks without committing.

Check 1 — YAML is valid:

Terminal window
python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/deploy.yml
echo "YAML OK"

Check 2 — Confirm the API key can create a session (dry run):

Terminal window
curl -s -X POST https://api.reoclo.com/external-deploy/session \
-H "Authorization: Bearer $REOCLO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"container_names": ["YOUR_CONTAINER_NAME"]}' \
| jq '{session_id, applications: [.applications[].linked_container_name], unmatched}'

Replace YOUR_CONTAINER_NAME with the container name from your Compose file or docker run command.

Expected shape:

{
"session_id": "uuid",
"applications": ["your-container-name"],
"unmatched": []
}

If applications is empty and unmatched contains your container name, the Application’s linked_container_name does not match. Inform the user and stop.

Check 3 — Clean up the test session:

Terminal window
SESSION_ID=$(curl -s -X POST https://api.reoclo.com/external-deploy/session \
-H "Authorization: Bearer $REOCLO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"container_names": ["YOUR_CONTAINER_NAME"]}' \
| jq -r '.session_id')
curl -s -X DELETE "https://api.reoclo.com/external-deploy/session/$SESSION_ID" \
-H "Authorization: Bearer $REOCLO_API_KEY" \
-o /dev/null -w "%{http_code}"

Expected: 204

  • Do not add application_id as an action input. The action does not accept an application_id. Apps are resolved by matching the service name in the Compose file to the Application’s linked_container_name on the server. There is no application ID input.
  • Do not pass server_id to reoclo/deploy-sync@v2. The action does not accept server_id. Server association is resolved via the session, which is scoped to the tenant of the rca_ key.
  • Do not rename the step to avoid conflicts. The step name is cosmetic; it does not affect behavior.
  • force: true is a valid action input as of @v2. Add it to the reoclo/deploy-sync@v2 step to override the 30-second conflict guard for that run — use it only for legitimate hotfixes, since a conflict result otherwise fails the step. (Passing force per-deployment to POST /external-deploy/sync directly also works.)
  • Token prefix matters. The api_key input must be an rca_ key (Automation Key). Tenant Integration Keys (rk_t_*) have been retired and are rejected by the API.

Auth: Authorization: Bearer rca_...

Request:

{
"container_names": ["string"],
"workflow_run_id": "string (optional)",
"commit_sha": "string (optional)"
}

Response 201:

{
"session_id": "uuid",
"session_token": "rds_...",
"expires_at": "2026-01-01T00:15:00Z",
"applications": [
{
"id": "uuid",
"linked_container_name": "string",
"container_port": 3000,
"bound_fqdns": ["example.com"]
}
],
"unmatched": ["container_name_with_no_app"]
}

Errors: 403 key lacks scope, 400 no container names matched.

Auth: Authorization: Bearer rds_...

Request:

{
"deployments": [
{
"container_name": "string",
"container_port": 3000,
"image_tag": "ghcr.io/org/repo:sha",
"force": false
}
]
}

Response 200:

{
"session_id": "uuid",
"results": [
{
"application_id": "uuid",
"container_name": "string",
"status": "synced | noop | conflict | drift_recovered",
"signature_hash": "sha256...",
"synced_fqdns": ["example.com"],
"reason": "string (present for conflict)"
}
],
"errors": [
{
"container_name": "string",
"reason": "string"
}
]
}

DELETE /external-deploy/session/{session_id}

Section titled “DELETE /external-deploy/session/{session_id}”

Auth: Bearer rca_... (matching tenant) OR Bearer rds_... (the session itself).

Response: 204 No Content