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.
What you need
Section titled “What you need”- The Quickstart finished — agent installed, device named
my-first, you’re logged in. - A
docker-compose.ymlfor your app, on the device, listening on a port. We’ll use port 3000 in this example.
The compose file
Section titled “The compose file”Use whatever you already have. For the sake of a complete example, here’s a minimal stack:
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, not0.0.0.0:3000. Edgible reaches the workload through the loopback interface; binding to0.0.0.0would 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
:latestand:16-alpine, and that’s fine. At deploy time Edgible resolves whatever a tag points to right now to a concretesha256:…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 timestack deployships 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 likepostgres:16-alpineare pulled as usual and never shipped.
The application YAML
Section titled “The application YAML”Save this as app.yml next to your docker-compose.yml:
apiVersion: v3kind: Applicationmetadata: 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.
Deploy
Section titled “Deploy”edgible stack deploy -f app.ymlWait for ready:
edgible stack status -f app.ymlOnce ready, the URL is in the status output. Visit it in a browser; your container responds.
Updating
Section titled “Updating”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:
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.
Rolling back
Section titled “Rolling back”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:
edgible application releases --app-id <app-id>Then re-point the deployment at an earlier release version:
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:
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.)
Tearing down
Section titled “Tearing down”edgible stack teardown -f app.ymlThe 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.