Skip to content

Recipe: Prepare a deploy folder on the server

Use this recipe when:

  • You are about to wire a reoclo/run@v2 step that runs docker compose up -d (or similar) inside a server-side directory.
  • The user has not yet decided where the Compose file lives on the server, or the existing folder is throwing Permission denied errors when the runner writes.
  • The user has a working reoclo/deploy-sync@v2 setup but every deploy involves manually chown-ing files afterward.

Do not use this recipe to set up the runner itself — installation is covered by the Runner install docs and provisions the folder + group automatically on Runner v1.24.0 and later.

The runner executes commands as the reoclo system user (rootful Docker) or as the rootless-Docker user (rootless mode). Human admins editing docker-compose.yml over SSH are a different user with a different primary group. Without a shared group, every cross-user edit needs a chown. Runner v1.24.0+ provisions /srv/reoclo/ with root:reoclo-deploy ownership and mode 2775 (setgid) — so files created anywhere inside inherit the reoclo-deploy group automatically. This recipe ensures the per-app subfolder is set up to take advantage of that.

Step 1 — Confirm the runner version is 1.24.0 or later:

The runner version is reported to Reoclo by the agent — there is no automation-key (rca_) REST endpoint for it. Read it with the CLI (which authenticates with your reoclo login profile, not the rca_ key) or from the Runner column on the Servers page in the dashboard:

Terminal window
reoclo servers get "$SERVER_ID" # shows the runner version in the record
# or, for scripting:
reoclo servers get "$SERVER_ID" -o json | jq -r '.runner_version'

If the version is older than 1.24.0, instruct the user to re-run the installer once:

Terminal window
curl -sSL https://get.reoclo.com/install.sh | bash -s -- --token <NEW_TOKEN>

Then re-run detection.

Step 2 — Determine the Docker mode of the host:

The installer provisions /srv/reoclo differently for rootful vs rootless Docker. The expected ownership and mode in Step 3 depend on this.

Terminal window
docker info --format '{{.SecurityOptions}}' 2>/dev/null
  • If the output contains name=rootlessrootless layout (no group, owner = the rootless user, mode 0755).
  • Otherwise → rootful layout (root:reoclo-deploy, mode 2775, setgid).

If docker info is not accessible, fall back to: stat -c '%G %a' /srv/reoclo — if the group is reoclo-deploy and the mode begins with 2, treat as rootful; if the group matches the runner’s user-group and the mode is 0755, treat as rootless.

Step 3 — Confirm /srv/reoclo exists with the expected ownership and mode:

Terminal window
stat -c '%U:%G %a' /srv/reoclo

Expected output, by mode:

Docker modeExpected stat output
Rootfulroot:reoclo-deploy 2775
Rootless<runner-user>:<runner-group> 755 (e.g. ubuntu:ubuntu 755)

If the output is root:root 755 (no chown happened), the installer did not run the v1.24.0 provisioning step. Re-run the installer (see Step 1).

Step 4 — Determine the app folder name:

Read the Compose file name and any nearby naming hints from the repository — typically the GitHub repo name or the value of --name in a docker run command. Use that as <app-name>. Do not invent a name; ask the user if the repo gives no clear signal.

The full path will be /srv/reoclo/<app-name>/.

Run on the server via reoclo/run@v2:

- name: Ensure deploy folder exists
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: |
mkdir -p /srv/reoclo/<app-name>

Why this works in both Docker modes:

  • Rootful: /srv/reoclo/ has setgid + root:reoclo-deploy, so the new subdirectory automatically inherits group reoclo-deploy and the setgid bit. The runner is in that group, so it can write.
  • Rootless: /srv/reoclo/ is owned by the rootless user (the same user the runner runs as), so the runner can mkdir directly. No group, no setgid.

In both cases, no chown or chmod is needed in this step.

- name: Verify deploy folder permissions
uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: |
stat -c '%U:%G %A' /srv/reoclo/<app-name>

Expected stdout:

  • Rootful: a line ending in a drwxrwsr-x mode string (the s in group-execute is the setgid bit). Group should be reoclo-deploy.
  • Rootless: a line ending in drwxr-xr-x, owner = the rootless user (same as the parent).

If the rootful output is wrong (e.g. reoclo:reoclo instead of root:reoclo-deploy), the parent’s setgid was not in effect at creation. Recover by:

Terminal window
sudo chgrp -R reoclo-deploy /srv/reoclo/<app-name>
sudo find /srv/reoclo/<app-name> -type d -exec chmod g+s {} \;

If the rootless output is wrong (e.g. owned by root from a stray sudo mkdir), recover by:

Terminal window
sudo chown -R <rootless-user>:<rootless-group> /srv/reoclo/<app-name>

This step depends on how the user delivers files to the server. Two common shapes:

(C1) Compose file already in the repo, runner pulls it via reoclo/checkout@v2:

- uses: reoclo/checkout@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
target_directory: /srv/reoclo/<app-name>

(C2) Compose file written inline by a reoclo/run@v2 step:

- uses: reoclo/run@v2
with:
api_key: ${{ secrets.REOCLO_API_KEY }}
server_id: ${{ secrets.REOCLO_SERVER_ID }}
command: |
cat > /srv/reoclo/<app-name>/docker-compose.prod.yml <<'EOF'
# ... compose content ...
EOF

Both work because the runner is the file’s creator, runs with UMask=0002, and /srv/reoclo/<app-name>/ has the inherited setgid bit — so the file ends up reoclo:reoclo-deploy 0664.

Step D — Update the deploy step to cd into the new folder

Section titled “Step D — Update the deploy step to cd into the new folder”

In the same workflow, find the reoclo/run@v2 step that runs docker compose up and ensure it cds into /srv/reoclo/<app-name>/:

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

If the deploy step previously used /opt/myapp or any other path, update it here.

Check 1 — Folder exists with the correct group:

Terminal window
stat -c '%U:%G %a' /srv/reoclo/<app-name>

Expected: root:reoclo-deploy 2775 (the leading 2 means setgid is set).

Check 2 — A test file created in the folder inherits the group:

Terminal window
touch /srv/reoclo/<app-name>/.test-perm
stat -c '%U:%G %a' /srv/reoclo/<app-name>/.test-perm
rm /srv/reoclo/<app-name>/.test-perm

Expected: group is reoclo-deploy. If the group is reoclo (the user’s primary group) or any other value, the setgid bit on the parent is not active — see Step B recovery commands.

Check 3 — Runner can read the Compose file:

Terminal window
sudo -u reoclo cat /srv/reoclo/<app-name>/docker-compose.prod.yml >/dev/null && echo "READABLE"

Expected: READABLE. If this fails with Permission denied, the file mode is 0600 or 0640 — fix with chmod g+r <file>. Re-running the runner with UMask=0002 will prevent recurrence for files the runner creates.

  • Rootful — do not chown reoclo:reoclo. That breaks the shared-write contract — once the runner owns a file, a human admin editing it over SSH gets Permission denied. In rootful mode always rely on root:reoclo-deploy + setgid inheritance.
  • Rootless — do not chown to a different user than the rootless one. The runner runs as the rootless user; any other owner re-introduces the permission gap the layout was designed to remove.
  • Do not chmod 0700 or 0750 on /srv/reoclo/<app-name>/. The runner needs at least g+rwx (rootful) or u+rwx (rootless) on directories so it can read Compose files and write Docker state. The default mode inherited from the parent (2775 rootful, 0755 rootless) is correct.
  • Do not use /opt/<app-name>/ as the deploy folder. /opt/reoclo/ is reserved for the runner binary; cleaning up app data near it risks deleting the runner. Always use /srv/reoclo/<app-name>/. This applies in both Docker modes — the path is identical.
  • Do not skip the mkdir -p step on the assumption “the folder will just appear”. External-deploy workflows that run cd /srv/reoclo/myapp without ensuring the folder exists fail with cd: /srv/reoclo/myapp: No such file or directory on first run.
  • Do not add a chown step “just in case”. It will undo the inheritance / installer-provisioned ownership and reintroduce the original problem. If stat /srv/reoclo already shows the expected output from Step 3, leave the per-app folder alone.