Skip to content

Compose Deployments

When you use compose_file with reoclo/deploy-sync@v2, the action reads your Compose file to discover which services to sync. This page explains the discovery rules, the network requirements for Caddy to reach your containers, and worked examples for common setups.

The action qualifies a service for sync if it meets either of these criteria:

  1. The service lists reoclo-proxy under its networks.
  2. The service has the label reoclo.managed=true.

Services that meet neither criterion are silently ignored — they are not submitted to the API and do not appear in any output. This is intentional: databases, queue workers, and other internal services that should never be publicly routed are automatically excluded without any extra configuration.

Opting a service out (reoclo.managed=false)

Section titled “Opting a service out (reoclo.managed=false)”

If a sidecar must share the reoclo-proxy network (or otherwise matches a discovery criterion) but should never be routed, label it reoclo.managed=false. The opt-out takes precedence over both criteria above, so the service is skipped during discovery — never submitted to the API, and no “no exposed port” warning is logged:

services:
cache:
image: redis:7
networks: [reoclo-proxy] # shares the network for app reachability…
labels:
reoclo.managed: "false" # …but is explicitly excluded from routing

Prefer keeping internal services off reoclo-proxy entirely (a private bridge — see Keeping a private database network). Use reoclo.managed=false only when sharing the network is unavoidable.

For each qualifying service, the action picks a port in this order:

  1. The first entry under ports that maps a container port to a host port (e.g. "3000:3000" → port 3000).
  2. If no ports mapping is present, the first value under expose (e.g. expose: ["8080"] → port 8080).
  3. If neither is found, the service is skipped with a warning in the action log.

When in doubt, add an explicit expose entry for clarity even if you are not publishing the port to the host.

Once a service is discovered, Reoclo has to bind it to one of your Applications. It matches on identity, resolved in this order:

  1. Explicit application reference — a reoclo.app (your Application’s slug) or reoclo.app-id (its UUID) label. reoclo.app-id wins if both are present.
  2. Container name — matched against the Application’s linked_container_name, any of its linked_container_names aliases, or its linked_container_name_pattern glob.

The name Reoclo receives is the service’s explicit container_name: if set, otherwise the Compose service key. That is rarely the same as the running container name (docker compose up names it <project>-<service>-<index>, e.g. myapp-api-1), so binding by name alone is fragile. Prefer the reoclo.app label — it makes the binding explicit and survives any container-naming scheme:

services:
app:
image: ghcr.io/myorg/app:${{ github.sha }}
networks: [reoclo-proxy]
expose: ["3000"]
labels:
reoclo.app: my-application-slug # bind to this Application by slug

Managed Caddy routes traffic to containers by resolving the container name inside the reoclo-proxy Docker network. For this to work, each service that receives external traffic must be attached to the reoclo-proxy network.

Declare the network at the top level of your Compose file and attach it to each public-facing service:

networks:
reoclo-proxy:
external: true

The network must be external: true because Reoclo creates and owns the reoclo-proxy network on your server. If you declare it as a local network, Docker creates a second network with the same name scoped to your project and Caddy cannot see your containers.

A routed app can sit on both reoclo-proxy (for ingress) and a private bridge (for its database/cache) — the standard Compose topology. Put app on both networks and the sidecars on the private network only:

services:
app:
networks: [internal, reoclo-proxy] # DB over internal, ingress over reoclo-proxy
postgres:
networks: [internal] # private — never on reoclo-proxy
networks:
internal:
driver: bridge
reoclo-proxy:
external: true

Reoclo’s managed-proxy reconciliation is additive: it ensures the app is attached to reoclo-proxy and, when it has to recreate the container (for example to remove a stray host-port publish), it preserves every other network and alias the container had. Your database stays on the private internal network and is never exposed on the shared ingress network.

docker-compose.prod.yml
services:
portfolio-web:
image: ghcr.io/myorg/portfolio:${IMAGE_TAG:-latest}
restart: unless-stopped
expose:
- "4321"
networks:
- reoclo-proxy
networks:
reoclo-proxy:
external: true

The portfolio-web service is on reoclo-proxy so the action will include it in the sync request. The container exposes port 4321, which Caddy uses as the upstream target.

docker-compose.prod.yml
services:
portfolio-frontend:
image: ghcr.io/myorg/portfolio-frontend:${IMAGE_TAG:-latest}
restart: unless-stopped
expose:
- "4321"
networks:
- reoclo-proxy
portfolio-api:
image: ghcr.io/myorg/portfolio-api:${IMAGE_TAG:-latest}
restart: unless-stopped
expose:
- "8080"
networks:
- reoclo-proxy
- internal
portfolio-worker:
image: ghcr.io/myorg/portfolio-worker:${IMAGE_TAG:-latest}
restart: unless-stopped
networks:
- internal # not on reoclo-proxy — skipped by sync
postgres:
image: postgres:16
restart: unless-stopped
networks:
- internal # not on reoclo-proxy — skipped by sync
networks:
reoclo-proxy:
external: true
internal:
driver: bridge

In this setup only portfolio-frontend and portfolio-api are submitted to the sync API. The worker and Postgres are on an internal-only network and are never exposed through Caddy. portfolio-api stays on both reoclo-proxy and internal — Reoclo preserves the internal network so the API keeps reaching Postgres privately (see Keeping a private database network).

The action will produce two results entries — one for each service — each matched to the corresponding Reoclo Application (by the reoclo.app label or container name; see Matching a service to its Application). Each matched Application’s proxy route is updated independently.

depends_on controls startup order inside Docker Compose but has no effect on the Reoclo sync step. The action calls POST /external-deploy/sync after docker compose up -d returns, at which point Docker has already applied depends_on sequencing to bring containers up. Reoclo syncs whatever is running; it does not need to re-order anything.

This means you can rely on depends_on to ensure the API waits for the database before accepting connections, and Reoclo will simply record the final running state of each service.

Using labels instead of network membership

Section titled “Using labels instead of network membership”

If you cannot add the reoclo-proxy network to a service (for example, the service uses network_mode: host), opt in with a label instead:

services:
myapp:
image: ghcr.io/myorg/myapp:latest
network_mode: host
labels:
reoclo.managed: "true"
expose:
- "3000"

The label acts as an explicit opt-in regardless of network configuration. The container must still be reachable from Caddy at the port you declare — with network_mode: host this is typically the case since Caddy runs on the same host.

The image_tag field in the sync payload is used to compute the deduplication signature. If you always deploy latest, every run will produce a different image digest but the same tag string — Reoclo will see the tag as identical and return noop even if the underlying image changed.

Use a specific tag (commit SHA, semantic version, or date stamp) so each deploy produces a unique signature:

image: ghcr.io/myorg/myapp:${{ github.sha }}

Pass the tag as a build argument or environment variable in your workflow and substitute it into the Compose file before running docker compose up.