Skip to content

Deploy a Docker Compose app

Most real-world workloads come as a Docker Compose stack — an app container, maybe a database, an environment file, a couple of named volumes. This guide takes that exact shape and exposes it.

  • The Quickstart finished — agent installed, device named my-first, you’re logged in.
  • A docker-compose.yml for your app, on the device, listening on a port. We’ll use port 3000 in this example.

Use whatever you already have. For the sake of a complete example, here’s a minimal stack:

docker-compose.yml
services:
web:
image: ghcr.io/your-org/your-app:latest
ports:
- "127.0.0.1:3000:3000"
environment:
- DATABASE_URL=postgres://app:app@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:

A few things to note:

  • The web container binds to 127.0.0.1:3000, not 0.0.0.0:3000. Edgible reaches the workload through the loopback interface; binding to 0.0.0.0 would unnecessarily expose the port on the device’s other interfaces.
  • The database has no ports: block. It’s only accessible to the web container over the Compose network — exactly the topology you want.
  • The image tags are :latest and :16-alpine, and that’s fine. At deploy time Edgible resolves whatever a tag points to right now to a concrete sha256:… digest and pins that digest into an immutable release. The running container is then tracked by digest, not by tag — so a later rebuild of the same tag is detected as a real change (see Updating) rather than silently ignored.
  • Services with a build: context work too — no registry required. Build the image locally (docker compose build); at deploy time stack deploy ships every build-backed image to the device over the ingest channel (docker save → upload → docker load). The device brings the stack up without --build, so it finds the shipped image by tag; registry-pullable images like postgres:16-alpine are pulled as usual and never shipped.

Save this as app.yml next to your docker-compose.yml:

app.yml
apiVersion: v3
kind: Application
metadata:
name: my-app
organization: <your-org-id>
spec:
placement:
strategy: serving-device
deviceSelector:
deviceName: my-first
workloads:
- name: stack
type: compose
composeFile: ./docker-compose.yml
ports:
- { name: http, containerPort: 3000, protocol: tcp }
access:
- name: public
type: https
target: { workload: stack, port: http }
hostname: { generated: true }
tls: { managedBy: edgible }
policies:
auth: { modes: [none] }

The composeFile path is resolved relative to the application YAML. The agent will read it, ship it to the device, and run docker compose up -d against it.

Terminal window
edgible stack deploy -f app.yml

Wait for ready:

Terminal window
edgible stack status -f app.yml

Once ready, the URL is in the status output. Visit it in a browser; your container responds.

Change the image, the env vars, or the compose file itself and re-run stack deploy. Each deploy resolves the current image digests; if the resolved content differs from what’s running, the control plane cuts a new release — an immutable, digest-pinned snapshot of the spec — and moves the application’s deployment pointer to it. The agent on the device fetches that release, verifies it, and realizes exactly the pinned digests: containers whose definition or image digest changed are recreated, the rest are left alone.

Because a release is keyed on resolved content (the spec plus the concrete image digests), rebuilding and re-pushing the same :latest tag is now a real update — the new bytes resolve to a new digest, which cuts a new release that reaches the device. Re-running stack deploy with genuinely identical content (same spec, same digests) is a no-op: no new release, nothing to converge.

After a deploy, gate on convergence rather than sleeping:

Terminal window
edgible application rollout status --app-id <app-id>

It reports each device’s progress toward the current release and exits 0 only when every device has fully realized it (1 while any is pending), so it drops straight into a deploy script as the “did the update actually land” check.

Every release the application has ever run is retained, so rolling back is a pointer move to an earlier release — not a new deploy, and not a new version. List the releases to find the one you want:

Terminal window
edgible application releases --app-id <app-id>

Then re-point the deployment at an earlier release version:

Terminal window
edgible application rollback <version> --app-id <app-id>

Rollback moves the deployment pointer backwards to that existing release; it does not cut a new version or re-resolve any tags — the device realizes exactly the digest-pinned bytes the release was cut with. Watch it converge with:

Terminal window
edgible application rollout status --app-id <app-id>

To force the current release to be re-realized on the device without changing anything — for example after clearing local state — use edgible application redeploy --app-id <app-id>.

(You can also keep the previous app.yml in version control and stack deploy it again. But note the difference: re-deploying old YAML re-resolves its image tags fresh, so a :latest may pin different bytes than the original run. rollback reproduces the exact bytes a release was cut with; re-deploying old YAML reproduces the exact spec.)

Terminal window
edgible stack teardown -f app.yml

The compose stack is stopped and removed, the public route is released, and the certificate is left in place for a short retention window in case you redeploy.