Skip to content

GitHub Actions Integration

Reoclo provides three GitHub Actions for integrating your CI/CD workflows with your servers:

  • reoclo/run - Execute commands on your servers
  • reoclo/checkout - Clone or update repositories on your servers
  • reoclo/docker-auth - Log in to a private container registry on your server using a vaulted Reoclo credential

There’s also reoclo/deploy-sync for syncing proxy routes after an external deploy.

Your GitHub Actions workflow orchestrates the steps, Reoclo dispatches commands to your servers, and every operation is fully audited.

  1. GitHub Actions runs your workflow on a GitHub-hosted runner
  2. The reoclo/run action sends commands to the Reoclo API
  3. Reoclo dispatches the command to the runner agent on your server via WebSocket
  4. The runner executes the command and returns the result
  5. Every operation is logged in Reoclo’s audit trail

This means you don’t need to run self-hosted GitHub runners on your servers. Your servers only need the Reoclo runner agent installed.

In the Reoclo dashboard:

  1. Navigate to the API Keys page, then select the Automation Keys tab
  2. Click Create Key
  3. Give it a descriptive name (e.g., github-prod-deploy)
  4. Configure the scope:
    • Servers: select which servers this key can target, or leave empty for all servers
    • Operations: select which operations are allowed (exec, deploy, restart, reboot)
  5. Optionally configure:
    • IP allowlist: restrict to GitHub-hosted runner IP ranges for extra security
    • Rate limit: requests per minute (default: 100)
    • Expiration: set an expiry date
  6. Click Create
  7. Copy the key immediately. It’s shown only once.

In your GitHub repository:

  1. Go to Settings > Secrets and variables > Actions
  2. Add two secrets:
    • REOCLO_API_KEY: the automation API key you just created
    • REOCLO_SERVER_ID: the ID of the target server (found in Servers page in the dashboard)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
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

Execute shell commands on your servers.

InputRequiredDefaultDescription
api_keyYes-Reoclo automation API key
server_idYes-Target server ID
commandYes-Shell command to execute on the server
working_directoryNo-Working directory on the server
envNo-Environment variables (KEY=VALUE, one per line)
timeoutNo60Timeout in seconds (max 900)
api_urlNohttps://api.reoclo.comReoclo API URL (for self-hosted instances)
OutputDescription
exit_codeCommand exit code
stdoutCommand stdout (truncated to 64KB)
stderrCommand stderr (truncated to 64KB)
operation_idReoclo operation ID for audit trail
duration_msExecution duration in milliseconds

Log in to a container registry on your server using a registry credential stored in your Reoclo tenant. The login runs on the target server, not on the GitHub runner, so subsequent reoclo/run steps can pull or push from the registry without any extra setup. When the job ends, an automatic cleanup step runs docker logout on the server.

Unlike docker/login-action@v3, this action sources the password from your Reoclo tenant instead of GitHub Secrets. You get one place to rotate credentials, per-key scoping, and a full audit trail.

InputRequiredDefaultDescription
api_keyYes-Reoclo automation API key
server_idYes-Target server ID (must be runner-connected)
credential_idConditional-Reoclo registry credential UUID, from the Registry Credentials page (vault mode). Required unless you use passthrough mode.
usernameConditional-Registry username (passthrough mode). Provide together with access_token and registry_url.
access_tokenConditional-Registry access token or password (passthrough mode). Masked in logs immediately on read.
registry_urlConditional-Registry URL for passthrough mode, e.g. ghcr.io or an ECR host.
cleanupNotrueRun docker logout on the server at job end
api_urlNohttps://api.reoclo.comReoclo API URL (for self-hosted instances)
OutputDescription
operation_idReoclo operation ID for the login (cross-reference with the audit log)
registry_urlResolved registry URL (for example, ghcr.io, docker.io, or your ECR host)
registry_typeRegistry provider (docker_hub, ghcr, aws_ecr, google_artifact_registry, azure_acr, harbor, generic)

The action supports two ways to supply the registry credential:

  • Vault mode (credential_id) — reference a credential stored in your Reoclo tenant. The password never leaves Reoclo; rotate it once in the dashboard and every workflow picks up the new value. Best for stable, long-lived credentials. The automation key must list the credential in its allowed credentials.
  • Passthrough mode (username + access_token + registry_url) — supply a username and token directly, for ephemeral tokens that can’t be vaulted ahead of time (e.g. ${{ secrets.GITHUB_TOKEN }} for GHCR, or a short-lived aws ecr get-login-password token). The token is masked in workflow logs immediately on read and carried to the server as ciphertext. The automation key must have allow_registry_passthrough enabled (dashboard, or PATCH /api-keys/{id}).

credential_id and the passthrough trio are mutually exclusive — provide one or the other, and supply all three passthrough fields together.

- name: Log in to GHCR with an ephemeral token
uses: reoclo/docker-auth@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
username: ${{ github.actor }}
access_token: ${{ secrets.GITHUB_TOKEN }}
registry_url: ghcr.io
  1. Create a Registry Credential in the dashboard. Pick a provider, enter the username and password or token, then save.
  2. Create an Automation API Key (or edit an existing one). Add registry_login (and registry_logout for the cleanup step) to Allowed Operations, and include the credential’s UUID in Allowed Credentials.
  3. On the Registry Credentials page, click Use in CI on the credential row. The drawer generates a pre-filled workflow snippet you can copy straight into your repository.
name: Build and push
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Log in to GHCR on the server
uses: reoclo/docker-auth@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
credential_id: ${{ secrets.REOCLO_GHCR_CREDENTIAL_ID }}
- name: Checkout code on server
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
path: /srv/reoclo/myapp
token: ${{ github.token }}
- name: Build and push
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
working_directory: /srv/reoclo/myapp
command: |
docker build -t ghcr.io/myorg/myapp:${{ github.sha }} .
docker push ghcr.io/myorg/myapp:${{ github.sha }}
timeout: 600

Use one step per credential. Each step creates its own login and logout in the audit log, so partial failures stay scoped to a single credential.

- uses: reoclo/docker-auth@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
credential_id: ${{ secrets.REOCLO_GHCR_CREDENTIAL_ID }}
- uses: reoclo/docker-auth@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
credential_id: ${{ secrets.REOCLO_ECR_CREDENTIAL_ID }}
  • The registry password is never returned to the GitHub Actions runner. Only the resolved registry URL, the registry provider type, and a Reoclo operation ID are set as outputs.
  • Every login and logout is recorded in your Reoclo audit log with the originating repository, workflow, actor, and commit.
  • Automation API keys must explicitly allow each credential they are permitted to use. Keys cannot reference credentials outside your tenant.
  • When cleanup is enabled (the default), docker logout runs automatically on the server at job end so the credential does not stay resident on the server filesystem.

Clone or update a repository on your server. Works like actions/checkout, but the code ends up on your remote server instead of the GitHub runner.

InputRequiredDefaultDescription
api_keyYes-Reoclo automation API key
server_idYes-Target server ID
repositoryNoCurrent repoRepository to checkout (owner/repo)
refNoCurrent SHABranch, tag, or SHA to checkout
pathNo/opt/deploy/workspaceDirectory on the server to checkout into. For deploys, set this to your app’s /srv/reoclo/<app-name> path so checkout and Compose share one location.
tokenNogithub.tokenToken for repository access
cleanNotruerm -rf the target path (including .git) before fetching. Set to false for incremental git fetch updates against an existing clone.
depthNo1Fetch depth for the requested ref. 0 means full history. The action fetches the ref directly, so any SHA on any branch resolves even at depth: 1.
fetch_tagsNofalseAlso fetch tags, even when depth > 0.
filterNoemptyPartial-clone filter applied to fetch (e.g. blob:none, tree:0). Pairs well with sparse_checkout.
sparse_checkoutNoemptySparse checkout patterns, one per line. Empty = full working tree.
sparse_checkout_cone_modeNotrueUse cone mode for sparse checkout.
submodulesNofalseCheckout submodules (true, false, or recursive)
lfsNofalseDownload Git LFS objects after checkout.
persist_credentialsNotrueKeep the token in .git/config after checkout. Set false to scrub it.
github_server_urlNo$GITHUB_SERVER_URL or https://github.comBase URL for GitHub. Use your GHES URL for GitHub Enterprise Server.
api_urlNohttps://api.reoclo.comReoclo API URL (for self-hosted instances)
OutputDescription
commit_shaThe checked-out commit SHA on the server (40 chars)
commitAlias for commit_sha, matching actions/checkout naming
short_shaFirst 7 chars of the commit SHA — useful for Docker tags (myapp:${{ steps.checkout.outputs.short_sha }})
refThe ref that was checked out
pathThe path on the server where the repo was checked out

Use Bitwarden, 1Password, or any secrets manager in GitHub Actions. Secrets are fetched in the workflow and passed to Reoclo as environment variables. They never touch Reoclo’s database.

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Fetch secrets from Bitwarden
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
secrets: |
abc123 > DB_URL
- name: Deploy with secrets
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: docker compose -f docker-compose.prod.yml up -d
working_directory: /srv/reoclo/myapp
env: |
DB_URL=${{ env.DB_URL }}
timeout: 300

Use reoclo/checkout to clone your code onto the server, then reoclo/run to build and deploy:

steps:
- name: Checkout code on server
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
path: /srv/reoclo/myapp
token: ${{ github.token }}
- name: Build and deploy
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
working_directory: /srv/reoclo/myapp
command: |
docker build -t myapp:latest .
docker compose -f docker-compose.prod.yml up -d
timeout: 600
- name: Checkout staging branch
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
ref: develop
path: /srv/reoclo/myapp-staging
token: ${{ github.token }}

When a workflow runs from a non-default branch (for example, push to staging), github.sha points at a commit on that branch — not on the default branch. From reoclo/[email protected] onwards, the action fetches the requested ref directly, so this works out of the box at depth: 1:

on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout the pushed commit on the server
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
# ref defaults to github.sha — explicit here for clarity
ref: ${{ github.sha }}
token: ${{ github.token }}

If you are pinned to reoclo/[email protected] or earlier, pass ref: ${{ github.ref_name }} and depth: 0 as a workaround, or upgrade to @v1.0.2+.

Route pushes on main to production and pushes on staging to your staging server. Each environment uses its own server ID, deploy path, and API key scope:

name: Deploy
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
env:
ENVIRONMENT: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
SERVER_ID: ${{ github.ref_name == 'main' && secrets.REOCLO_PROD_SERVER_ID || secrets.REOCLO_STAGING_SERVER_ID }}
SERVER_PATH: /srv/reoclo/myapp-${{ github.ref_name == 'main' && 'production' || 'staging' }}
steps:
- name: Sync repo onto the server
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ env.SERVER_ID }}
path: ${{ env.SERVER_PATH }}
ref: ${{ github.sha }}
token: ${{ github.token }}
- name: Pull and roll containers
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ env.SERVER_ID }}
working_directory: ${{ env.SERVER_PATH }}
command: |
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
timeout: 600

Use a Personal Access Token with cross-repo access:

- name: Checkout shared config
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
repository: myorg/shared-config
path: /srv/reoclo/shared-config
token: ${{ secrets.CROSS_REPO_TOKEN }}
on:
workflow_dispatch:
inputs:
service:
description: Service to deploy
type: choice
options: [api, web, worker]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy service
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 ${{ inputs.service }}
docker compose -f docker-compose.prod.yml up -d ${{ inputs.service }}
timeout: 300

Combine reoclo/checkout and multiple reoclo/run steps for multi-stage deployments:

steps:
- name: Checkout latest code
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
path: /srv/reoclo/myapp
token: ${{ github.token }}
- name: Run migrations
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
working_directory: /srv/reoclo/myapp
command: docker compose -f docker-compose.prod.yml exec -T api python manage.py migrate
timeout: 120
- name: Build and restart services
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
working_directory: /srv/reoclo/myapp
command: |
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
timeout: 600

For faster deploys, skip the full clone and fetch updates instead:

- name: Update code on server
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
clean: false
token: ${{ github.token }}

If the repository already exists at the target path, the action runs git fetch and checks out the ref instead of re-cloning. This is faster for large repositories.

For a monorepo where each service has its own deploy workflow, fetch only the subtrees that service needs. Combine with filter: blob:none for a partial clone that downloads file contents on demand:

- name: Checkout just the API service
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
sparse_checkout: |
services/api
packages/shared
docker-compose.prod.yml
filter: blob:none

On a 5 GB monorepo this typically shrinks the on-server checkout to tens of megabytes.

Download Git LFS objects (large binaries tracked by git-lfs) after the checkout completes:

- uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
lfs: 'true'

The Reoclo runner agent must have git-lfs installed on the target server. Install with apt-get install git-lfs (Debian/Ubuntu) or dnf install git-lfs (RHEL/Fedora).

Set github_server_url to your GHES base URL. The action derives the clone URL from it. $GITHUB_SERVER_URL is auto-populated on GHES runners, so this is only needed when overriding:

- uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
github_server_url: https://github.acme-corp.com
token: ${{ secrets.GHES_PAT }}

By default the action embeds the token in the on-server .git/config so subsequent git fetch calls authenticate without re-supplying it. If you don’t want the token resident on the server (e.g. for long-lived deploy paths shared across workflows), opt out with persist_credentials: false — the action will rewrite origin to the bare URL after checkout:

- uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
persist_credentials: 'false'
- name: Checkout on server
id: checkout
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
path: /srv/reoclo/myapp
- name: Build and tag image
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
working_directory: /srv/reoclo/myapp
command: |
docker build -t myapp:${{ steps.checkout.outputs.short_sha }} .
docker tag myapp:${{ steps.checkout.outputs.short_sha }} myapp:latest
timeout: 600
- name: Checkout with submodules
uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
depth: 0
submodules: recursive
token: ${{ github.token }}

Every command executed via the GitHub Action is logged in Reoclo’s audit trail. Each operation records:

  • The command that was executed
  • Which server it ran on
  • The exit code, stdout, and stderr
  • Which API key was used
  • The GitHub Actions run context (repository, workflow, actor, commit SHA)

View automation operations in the dashboard by clicking the operation count on any key row to view history in Logs.

Operations from the same GitHub Actions run are grouped by run_id, so you can see all commands from a single workflow execution together.

Always scope your automation API keys to the minimum required permissions:

  • Restrict to specific servers rather than all servers
  • Limit operations to what the workflow needs (e.g., exec only)
  • Set an IP allowlist to GitHub’s runner IP ranges
  • Use separate keys for different environments (staging vs production)

Environment variables passed via the env input are:

  • Sent to the server for command execution
  • Never stored in Reoclo’s database. Only the key names appear in audit logs.
  • Transmitted over HTTPS to the Reoclo API, then over the encrypted WebSocket to the runner

Each API key has a configurable rate limit (default: 100 requests/minute). If exceeded, the action will receive a 429 Too Many Requests response.

Unable to resolve action <owner>/<action>@<tag>, unable to find version <tag>

Section titled “Unable to resolve action <owner>/<action>@<tag>, unable to find version <tag>”

The pinned action version tag does not exist. Either pin to an exact released version (e.g. reoclo/[email protected]) or use the floating major-version tag (@v2). Check the latest releases on GitHub to find the current tag.

The action is pointed at a URL that does not serve the automation API. The default api_url in the action targets https://api.reoclo.com. If you overrode api_url in your workflow, make sure it points at the Reoclo API host for your tenant.

A server-side error reached the action. Check the run’s operation_id output, then look up the operation in the Reoclo dashboard under Activity to see the full stack trace. If the error is not specific to your server, it may be a platform incident.

Checkout failed: ... fatal: unable to read tree (<sha>)

Section titled “Checkout failed: ... fatal: unable to read tree (<sha>)”

The on-server git checkout could not find the tree for the requested SHA. This was the default failure mode on reoclo/[email protected] and earlier when deploying from a non-default branch: the action did git clone --depth 1 of the default branch and then tried to check out a SHA that was not in that shallow clone.

Fixes, in order of preference:

  1. Upgrade. reoclo/[email protected]+ fetches the requested ref directly, so any SHA on any branch resolves at depth: 1. Bump your uses: line and rerun.
  2. Workaround on older versions. Pin ref: ${{ github.ref_name }} and depth: 0 to force a full-history clone of the branch the workflow was triggered on:
    - uses: reoclo/checkout@v2
    with:
    ref: ${{ github.ref_name }}
    depth: 0
    # ...
  3. Confirm the token has access. A 403-style fetch failure can sometimes surface as a tree-read error on the next step. Make sure token resolves to a token with read access to the repository.

command not found: docker inside the action

Section titled “command not found: docker inside the action”

The runner is executing as a user that does not have Docker on its PATH. Install Docker on the server (or use rootless Docker) and verify with:

Terminal window
- name: Check Docker
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: which docker && docker version

The deploy succeeds but https://myapp.example.com returns 404 or the default Caddy page

Section titled “The deploy succeeds but https://myapp.example.com returns 404 or the default Caddy page”

Your container is running, but managed Caddy is not yet routing the domain to it. Open the server’s Proxy tab in the dashboard, find your container under Route candidates, click Route this, enter the FQDN, and confirm. See the Wire a route section below.

The workflow hangs on Executing command on server...

Section titled “The workflow hangs on Executing command on server...”

The action waits synchronously for command completion. If your command takes longer than the configured timeout, the action fails with a timeout error. Increase the timeout input on the step (maximum 900 seconds).

After a workflow deploys a container, it is reachable on the server but not yet attached to a domain. To expose it at https://myapp.example.com, wire a managed Caddy route:

  1. Open the server in the Reoclo dashboard and go to the Proxy tab.
  2. Find the container under Route candidates.
  3. Click Route this and enter the FQDN.
  4. Within ~30 seconds, the route appears under Active Routes and managed Caddy starts serving the container.

This is a one-time step per container + domain pair. Future deploys that replace the container leave the route intact as long as the container keeps the same name.

reoclo/gh-action-tests is a live reference that exercises every action and pattern documented here across seven workflows:

  • test1: simplest possible deploy (nginx:alpine, no build).
  • test2: reoclo/checkout + nginx with a volume-mounted static site.
  • test3: build-on-server with docker compose.
  • test4: private GHCR image via reoclo/docker-auth.
  • test5: multi-container compose with env secrets.
  • test6: large build stressing the build timeout and buildkit cache.
  • test7: health-gated blue/green rollout with rollback.

Clone the repo to model your own workflows, or fork it to run the same tests against your own server.