Recipe: Enhance an existing GitHub workflow with Reoclo route sync
When to use this
Section titled “When to use this”Use this recipe when:
- A
.github/workflows/*.ymlfile exists in the repository. - At least one workflow step contains
docker compose,docker run, ordocker stack. - The user has a Reoclo account and at least one Application with a
linked_container_nameconfigured.
Do not use this recipe if the repository has no existing Docker workflow. Use Set Up External Deployment instead.
Detection
Section titled “Detection”Before making any changes, confirm the recipe applies.
Step 1 — Find workflow files with Docker commands:
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:
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
201with your container underapplications→ the recipe can proceed. - Your container under
unmatched(or an emptyapplications) → no Application has a matchinglinked_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.
The change
Section titled “The change”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: 300After:
- 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.ymlRules for placing the step:
- Always insert after the last
docker compose up/docker runstep 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.
Verification
Section titled “Verification”After editing the workflow file, run these checks without committing.
Check 1 — YAML is valid:
python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/deploy.ymlecho "YAML OK"Check 2 — Confirm the API key can create a session (dry run):
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:
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
Pitfalls
Section titled “Pitfalls”- Do not add
application_idas an action input. The action does not accept anapplication_id. Apps are resolved by matching the service name in the Compose file to the Application’slinked_container_nameon the server. There is no application ID input. - Do not pass
server_idtoreoclo/deploy-sync@v2. The action does not acceptserver_id. Server association is resolved via the session, which is scoped to the tenant of therca_key. - Do not rename the step to avoid conflicts. The step name is cosmetic; it does not affect behavior.
force: trueis a valid action input as of@v2. Add it to thereoclo/deploy-sync@v2step to override the 30-second conflict guard for that run — use it only for legitimate hotfixes, since aconflictresult otherwise fails the step. (Passingforceper-deployment toPOST /external-deploy/syncdirectly also works.)- Token prefix matters. The
api_keyinput must be anrca_key (Automation Key). Tenant Integration Keys (rk_t_*) have been retired and are rejected by the API.
API contract reference
Section titled “API contract reference”POST /external-deploy/session
Section titled “POST /external-deploy/session”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.
POST /external-deploy/sync
Section titled “POST /external-deploy/sync”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