Ymir Egilson

Ymir Egilson

Tech

How to fix the "array to string conversion" error in coolify docker compose

How to fix the "array to string conversion" error in coolify docker compose

How to fix the "array to string conversion" error in coolify docker compose deployments

If you have ever attempted to import a perfectly-valid docker-compose.yml into Coolify (or another PHP-based tool such as Portainer, Shipyard, Rancher 1.x) and been greeted by the notice above, you have already discovered an odd corner where Docker Compose ≠ YAML ≠ PHP array handling.

In this post we will:

  • Reproduce the problem with a minimal snippet
  • Walk through five common trouble spots
  • Show the before → after diff that cures the warning
  • Explain why older PHP Compose parsers choke and what to do if you cannot change the file format

The smoking gun

Here's a minimal example that triggers the error:

services:
  app:
    command:
      - unicorn
      - myapp.py
      - --bind
      - :8000 # ← list-form command
    ports: # ← long-form ports
      - mode: ingress
        target: 8000
        published: '8080'
        protocol: tcp

Feed that file into Coolify 4.0-beta 419 and the container never starts; the logs show:

PHP Notice:  Array to string conversion in /…/vendor/symfony/yaml/Inline.php:290

Compose itself (docker compose config) is perfectly happy. The problem lies in Coolify's vintage Compose-to-PHP converter, which was written for the v2 spec (2017) where many fields were always scalars.

1 — Extended depends_on objects → simple string list

🔴 Problem

depends_on:
  db:
    condition: service_healthy
    required: true

The PHP helper tries to implode() that object.

✅ Fix

depends_on:
  - db

Loss of behaviour?
None for most setups: Coolify doesn't wait on health-checks anyway. If you need the conditional version, keep your object form but upgrade to Coolify ≥ 4.1 where the new Go parser has landed.

2 — Long-form ports, volumes, networks → short strings

🔴 Problem

ports:
  - mode: ingress
    target: 80
    published: '8080'
    protocol: tcp

✅ Fix

ports:
  - '8080:80'

Same pattern for volumes and service-scoped networks:

- volumes:
-   - type: volume
-     source: app_data
-     target: /data
+ volumes:
+   - app_data:/data

3 — Array-form command / healthcheck.test → folded scalars

🔴 Problem

command:
  - sh
  - -c
  - 'python app.py'

✅ Fix

command: >-
  sh -c "python app.py"

Why it works: the Symfony YAML component inside Coolify converts a folded block scalar into a PHP string – the type the next layer expects.

4 — Empty mapping stubs → delete them

YAML treats {} as an empty map, which becomes array() in PHP. If a later helper concatenates that value you hit the notice.

🔴 Problem

volume: {}

✅ Fix

Just remove the key entirely:

# nothing here

Same for unused top-level sections:

- configs: {}
- secrets: {}

5 — Explicit external networks & restart policies

Older Coolify releases implicitly converted unknown networks to strings, so make external intent clear:

networks:
  coolify-proxy:
    external: true

One-shot jobs (migration, setup_config) must not inherit restart: always from copy-paste. Use the spec value:

restart: 'no'

Putting it all together

Below is a fully-working Compose file for our Hatchet stack after applying every fix. You can drop it into Coolify ≤ 4.0 and the PHP warning disappears:

name: aevy_hatchet
 
networks:
  coolify-proxy:
    external: true
 
volumes:
  aevy_hatchet_certs:
  aevy_hatchet_config:
  aevy_hatchet_postgres_data:
  aevy_hatchet_rabbitmq_conf:
  aevy_hatchet_rabbitmq_data:
 
services:
  aevy_hatchet_dashboard:
    image: ghcr.io/hatchet-dev/hatchet/hatchet-dashboard:latest
    command: >-
      sh ./entrypoint.sh --config /hatchet/config
    depends_on:
      - migration
      - setup_config
    environment:
      DATABASE_URL: postgres://hatchet:hatchet@aevy_hatchet_postgres:5432/hatchet
    healthcheck:
      disable: true
    labels:
      traefik.enable: 'true'
      traefik.http.routers.hatchet.entrypoints: websecure
      traefik.http.routers.hatchet.rule: Host(`hatchet.aevy.app`)
      traefik.http.routers.hatchet.tls.certresolver: letsencrypt
      traefik.http.services.hatchet.loadbalancer.server.port: '80'
    networks:
      - coolify-proxy
    ports:
      - '8001:80'
    restart: on-failure
    volumes:
      - aevy_hatchet_certs:/hatchet/certs
      - aevy_hatchet_config:/hatchet/config
  # … (other services omitted for brevity; see full listing in repo)

Why does PHP trip but Docker doesn't?

  • Docker uses docker/compose-go (Go structs)
  • Coolify ≤ 4.0 used Symfony YAML → PHP arrays → DTOs
  • The legacy DTOs expect scalar strings in many places; when Symfony hands them an array they fall back to __toString() which produces the notice
  • Upgrading to Coolify 4.1+ (or any tool that embeds the official compose-spec library) fixes the root cause
  • Until you can, the five edits above are a reliable workaround

TL;DR / Cheatsheet

CulpritBad exampleGood example
Extended depends_onobjectlist
Long-form ports/volumesmapping"host:container"
List-form commandarrayfolded scalar
Empty maps(delete)
Implicit network / wrong restartmissing / defaultexternal: true / "no"

You can likely paste this into an LLM along with your docker-compose.yml and it will fix it for you.