From 0d22b30910df2d40f13121fc14a22c91f12f3397 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:01:07 +0100 Subject: [PATCH] fix(compose): parse mode suffixes in long-form bind targets --- AGENTS.md | 1 + bootstrap/helpers/parsers.php | 7 ++++++- templates/compose/dozzle-with-auth.yaml | 2 +- templates/service-templates-latest.json | 4 ++-- templates/service-templates.json | 4 ++-- tests/Feature/ComposeLongBindOptionsTest.php | 19 +++++++++++++++++++ 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bfa80f40cf..1ab68ce6b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -262,6 +262,7 @@ The persistent-storage implementation is the reference pattern: `Project\Service - Run `vendor/bin/pint --dirty --format agent` before finalizing changes - Every change must have tests — write or update tests, then run them. For bug fixes, follow TDD: write a failing test first, then fix the bug (see Test Enforcement below) - Check sibling files for conventions before creating new files +- Nixpacks is deprecated. Never recommend it to users (issue replies, docs, UI copy, workarounds). Recommend the Dockerfile build pack, or Railpack where it fits. - When adding remote shell commands, account for servers using non-root SSH users: commands pass through `parseCommandsByLineForSudo()`, so test pipelines, redirects, substitutions, and `sh -c`/`bash -c` scripts with the non-root sudo parser. ## Git Workflow diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 6b8b133f01..5e34a4dc4c 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -148,7 +148,8 @@ function composeResourceDirectory(Application|Service $resource): string * Renders a long-syntax bind with its resolved source. It keeps the options that Docker Compose * accepts, such as read_only and bind, and removes other keys, which Compose rejects. Without * `bind.create_host_path`, a missing source is created, as with short syntax: Docker Compose - * before v2.30 does not do that for long syntax. + * before v2.30 does not do that for long syntax. A short-syntax mode in the target + * (`/data/users.yml:ro`) becomes `read_only`, because Docker rejects it in long syntax. * * @param array $volume * @return array @@ -160,6 +161,10 @@ function composeLongBindVolume(array $volume, string $source): array fn ($key) => in_array($key, ['type', 'source', 'target', 'read_only', 'consistency', 'bind'], true) || str_starts_with((string) $key, 'x-'), ARRAY_FILTER_USE_KEY ); + if (preg_match('/^(.+):(ro|rw)$/', (string) ($volume['target'] ?? ''), $matches)) { + $volume['target'] = $matches[1]; + $volume['read_only'] ??= $matches[2] === 'ro'; + } $volume['source'] = $source; $volume['bind'] = is_array($volume['bind'] ?? null) ? $volume['bind'] : []; $volume['bind']['create_host_path'] ??= true; diff --git a/templates/compose/dozzle-with-auth.yaml b/templates/compose/dozzle-with-auth.yaml index 318301253f..b94c48457a 100644 --- a/templates/compose/dozzle-with-auth.yaml +++ b/templates/compose/dozzle-with-auth.yaml @@ -15,7 +15,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - type: bind source: ./data/users.yml - target: /data/users.yml:ro + target: /data/users.yml content: | users: # "admin" is the username diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index fdff157f2b..eb71bc397d 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1138,7 +1138,7 @@ "dozzle-with-auth": { "documentation": "https://dozzle.dev/?utm_source=coolify.io", "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", - "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPWlpMRV84MDgwCiAgICAgIC0gRE9aWkxFX0FVVEhfUFJPVklERVI9c2ltcGxlCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZGF0YS91c2Vycy55bWwKICAgICAgICB0YXJnZXQ6ICcvZGF0YS91c2Vycy55bWw6cm8nCiAgICAgICAgY29udGVudDogInVzZXJzOlxuICAjIFwiYWRtaW5cIiBpcyB0aGUgdXNlcm5hbWVcbiAgYWRtaW46XG4gICAgZW1haWw6IHRlc3RAZW1haWwuY29tXG4gICAgbmFtZTogQWRtaW5cbiAgICAjIEEgYmNyeXB0IGhhc2ggb2YgdGhlIHBhc3N3b3JkIHlvdSB3YW50IHRvIHVzZS4gQ2FuIGJlIGNvbXB1dGVkIHdpdGggXCJweXRob24zIC1jICdpbXBvcnQgYmNyeXB0O3ByaW50KGJjcnlwdC5oYXNocHcoYlwieW91ci1wYXNzd29yZFwiLGJjcnlwdC5nZW5zYWx0KDExKSkuZGVjb2RlKCkpJyBcIi4gRGVmYXVsdCBwYXNzd29yZCBpcyBcInRlc3RcIi5cbiAgICBwYXNzd29yZDogJDJhJDExJHZpdWNDdkZMbEhXdkJOT09JNnV5cHVWVS5EMDlVV2IuenN3UnhFZzBNa0RQaTFxL2JLYmRHXG4iCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2RvenpsZQogICAgICAgIC0gaGVhbHRoY2hlY2sKICAgICAgaW50ZXJ2YWw6IDNzCiAgICAgIHRpbWVvdXQ6IDMwcwogICAgICByZXRyaWVzOiA1Cg==", + "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPWlpMRV84MDgwCiAgICAgIC0gRE9aWkxFX0FVVEhfUFJPVklERVI9c2ltcGxlCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZGF0YS91c2Vycy55bWwKICAgICAgICB0YXJnZXQ6IC9kYXRhL3VzZXJzLnltbAogICAgICAgIGNvbnRlbnQ6ICJ1c2VyczpcbiAgIyBcImFkbWluXCIgaXMgdGhlIHVzZXJuYW1lXG4gIGFkbWluOlxuICAgIGVtYWlsOiB0ZXN0QGVtYWlsLmNvbVxuICAgIG5hbWU6IEFkbWluXG4gICAgIyBBIGJjcnlwdCBoYXNoIG9mIHRoZSBwYXNzd29yZCB5b3Ugd2FudCB0byB1c2UuIENhbiBiZSBjb21wdXRlZCB3aXRoIFwicHl0aG9uMyAtYyAnaW1wb3J0IGJjcnlwdDtwcmludChiY3J5cHQuaGFzaHB3KGJcInlvdXItcGFzc3dvcmRcIixiY3J5cHQuZ2Vuc2FsdCgxMSkpLmRlY29kZSgpKScgXCIuIERlZmF1bHQgcGFzc3dvcmQgaXMgXCJ0ZXN0XCIuXG4gICAgcGFzc3dvcmQ6ICQyYSQxMSR2aXVjQ3ZGTGxIV3ZCTk9PSTZ1eXB1VlUuRDA5VVdiLnpzd1J4RWcwTWtEUGkxcS9iS2JkR1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9kb3p6bGUKICAgICAgICAtIGhlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogNQo=", "tags": [ "dozzle", "docker", @@ -3509,7 +3509,7 @@ "category": "messaging", "logo": "svgs/mosquitto.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-05T18:25:15+01:00", + "template_last_updated_at": "2026-10-23T22:58:59+02:00", "port": "1883" }, "n8n-with-postgres-and-worker": { diff --git a/templates/service-templates.json b/templates/service-templates.json index 9cfcb9f28e..5d1b2c66f0 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1138,7 +1138,7 @@ "dozzle-with-auth": { "documentation": "https://dozzle.dev/?utm_source=coolify.io", "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", - "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET1paTEVfODA4MAogICAgICAtIERPWlpMRV9BVVRIX1BST1ZJREVSPXNpbXBsZQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEvdXNlcnMueW1sCiAgICAgICAgdGFyZ2V0OiAnL2RhdGEvdXNlcnMueW1sOnJvJwogICAgICAgIGNvbnRlbnQ6ICJ1c2VyczpcbiAgIyBcImFkbWluXCIgaXMgdGhlIHVzZXJuYW1lXG4gIGFkbWluOlxuICAgIGVtYWlsOiB0ZXN0QGVtYWlsLmNvbVxuICAgIG5hbWU6IEFkbWluXG4gICAgIyBBIGJjcnlwdCBoYXNoIG9mIHRoZSBwYXNzd29yZCB5b3Ugd2FudCB0byB1c2UuIENhbiBiZSBjb21wdXRlZCB3aXRoIFwicHl0aG9uMyAtYyAnaW1wb3J0IGJjcnlwdDtwcmludChiY3J5cHQuaGFzaHB3KGJcInlvdXItcGFzc3dvcmRcIixiY3J5cHQuZ2Vuc2FsdCgxMSkpLmRlY29kZSgpKScgXCIuIERlZmF1bHQgcGFzc3dvcmQgaXMgXCJ0ZXN0XCIuXG4gICAgcGFzc3dvcmQ6ICQyYSQxMSR2aXVjQ3ZGTGxIV3ZCTk9PSTZ1eXB1VlUuRDA5VVdiLnpzd1J4RWcwTWtEUGkxcS9iS2JkR1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9kb3p6bGUKICAgICAgICAtIGhlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogNQo=", + "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET1paTEVfODA4MAogICAgICAtIERPWlpMRV9BVVRIX1BST1ZJREVSPXNpbXBsZQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEvdXNlcnMueW1sCiAgICAgICAgdGFyZ2V0OiAvZGF0YS91c2Vycy55bWwKICAgICAgICBjb250ZW50OiAidXNlcnM6XG4gICMgXCJhZG1pblwiIGlzIHRoZSB1c2VybmFtZVxuICBhZG1pbjpcbiAgICBlbWFpbDogdGVzdEBlbWFpbC5jb21cbiAgICBuYW1lOiBBZG1pblxuICAgICMgQSBiY3J5cHQgaGFzaCBvZiB0aGUgcGFzc3dvcmQgeW91IHdhbnQgdG8gdXNlLiBDYW4gYmUgY29tcHV0ZWQgd2l0aCBcInB5dGhvbjMgLWMgJ2ltcG9ydCBiY3J5cHQ7cHJpbnQoYmNyeXB0Lmhhc2hwdyhiXCJ5b3VyLXBhc3N3b3JkXCIsYmNyeXB0LmdlbnNhbHQoMTEpKS5kZWNvZGUoKSknIFwiLiBEZWZhdWx0IHBhc3N3b3JkIGlzIFwidGVzdFwiLlxuICAgIHBhc3N3b3JkOiAkMmEkMTEkdml1Y0N2RkxsSFd2Qk5PT0k2dXlwdVZVLkQwOVVXYi56c3dSeEVnME1rRFBpMXEvYktiZEdcbiIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvZG96emxlCiAgICAgICAgLSBoZWFsdGhjaGVjawogICAgICBpbnRlcnZhbDogM3MKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDUK", "tags": [ "dozzle", "docker", @@ -3509,7 +3509,7 @@ "category": "messaging", "logo": "svgs/mosquitto.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-05T18:25:15+01:00", + "template_last_updated_at": "2026-10-23T22:58:59+02:00", "port": "1883" }, "n8n-with-postgres-and-worker": { diff --git a/tests/Feature/ComposeLongBindOptionsTest.php b/tests/Feature/ComposeLongBindOptionsTest.php index 7f65bb8ca7..b8d3011d15 100644 --- a/tests/Feature/ComposeLongBindOptionsTest.php +++ b/tests/Feature/ComposeLongBindOptionsTest.php @@ -235,3 +235,22 @@ it('keeps a ${VAR:-default} long-form bind source for Docker Compose when the va 'variable reference' => ['$BASE_DIR/ao.yaml'], 'vault reference' => ['{{vault.AO_CONFIG_FILE}}'], ]); + +it('moves a mode suffix from the target of a long-form bind to read_only', function (string $kind, string $mode, bool $readOnly) { + $result = parseLongBind($kind, <<toBe('/fixture') + ->and($result['volume']['read_only'])->toBe($readOnly); +})->with(['service', 'application'])->with([ + 'read-only' => ['ro', true], + 'read-write' => ['rw', false], +]);