Recipe: Prepare a deploy folder on the server
When to use this
Section titled “When to use this”Use this recipe when:
- You are about to wire a
reoclo/run@v2step that runsdocker 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 deniederrors when the runner writes. - The user has a working
reoclo/deploy-sync@v2setup but every deploy involves manuallychown-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.
Background (1 paragraph for context)
Section titled “Background (1 paragraph for context)”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.
Detection
Section titled “Detection”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:
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:
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.
docker info --format '{{.SecurityOptions}}' 2>/dev/null- If the output contains
name=rootless→ rootless layout (no group, owner = the rootless user, mode0755). - Otherwise → rootful layout (
root:reoclo-deploy, mode2775, 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:
stat -c '%U:%G %a' /srv/reocloExpected output, by mode:
| Docker mode | Expected stat output |
|---|---|
| Rootful | root: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>/.
The change
Section titled “The change”Step A — Create the folder (idempotent)
Section titled “Step A — Create the folder (idempotent)”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 groupreoclo-deployand 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 canmkdirdirectly. No group, no setgid.
In both cases, no chown or chmod is needed in this step.
Step B — Verify ownership inheritance
Section titled “Step B — Verify ownership inheritance”- 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-xmode string (thesin group-execute is the setgid bit). Group should bereoclo-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:
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:
sudo chown -R <rootless-user>:<rootless-group> /srv/reoclo/<app-name>Step C — Drop the Compose file in place
Section titled “Step C — Drop the Compose file in place”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 ... EOFBoth 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: 300If the deploy step previously used /opt/myapp or any other path, update it here.
Verification
Section titled “Verification”Check 1 — Folder exists with the correct group:
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:
touch /srv/reoclo/<app-name>/.test-permstat -c '%U:%G %a' /srv/reoclo/<app-name>/.test-permrm /srv/reoclo/<app-name>/.test-permExpected: 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:
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.
Pitfalls
Section titled “Pitfalls”- 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 getsPermission denied. In rootful mode always rely onroot:reoclo-deploy+ setgid inheritance. - Rootless — do not
chownto 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 0700or0750on/srv/reoclo/<app-name>/. The runner needs at leastg+rwx(rootful) oru+rwx(rootless) on directories so it can read Compose files and write Docker state. The default mode inherited from the parent (2775rootful,0755rootless) 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 -pstep on the assumption “the folder will just appear”. External-deploy workflows that runcd /srv/reoclo/myappwithout ensuring the folder exists fail withcd: /srv/reoclo/myapp: No such file or directoryon first run. - Do not add a
chownstep “just in case”. It will undo the inheritance / installer-provisioned ownership and reintroduce the original problem. Ifstat /srv/reocloalready shows the expected output from Step 3, leave the per-app folder alone.
Related recipes
Section titled “Related recipes”- Enhance an existing GitHub workflow with Reoclo route sync — for the sync step that pairs with this deploy folder.
- Set up External Deployment from scratch — for repos with no existing Reoclo workflow.
- Diagnose a deployment failure — if
reoclo/run@v2errors withPermission deniedafter this recipe runs.