Async Docker deployments to edge devices (without SSH)
With m87, you can deploy Docker Compose stacks to edge devices asynchronously.
You register a deployment once, and the device applies it as soon as it has connectivity — even if it’s offline, behind NAT, or reboots in between.
There is no SSH access, no inbound ports, and no CI runner that needs to reach the device.
The problem
Edge devices are often unavailable when you want to deploy:
- Devices reboot or lose connectivity
- Devices sit behind NAT or CGNAT
- SSH access is unreliable or intentionally disabled
- CI pipelines assume the device is reachable now
In these environments, “run this command on the device” is the wrong model.
The solution: async Docker deployments
m87 uses a deployment-based model:
- You register a Docker Compose deployment with m87
- The deployment is stored centrally
- The device pulls and executes it when it has internet access
- Execution state, retries, and failures are tracked automatically
Once registered, the deployment remains active and reconciles over time.
Quick start
1) Create and activate a deployment
m87 <device> deployment new --active
This creates a new deployment and marks it as the active desired state for the device.
2) Register your Docker Compose stack
From the directory containing docker-compose.yml:
m87 <device> deploy docker-compose.yml
This uploads a run spec based on your compose file.
3) Monitor execution and status
m87 <device> deployment status --logs
You can disconnect at any time — execution continues on the device.
What happens under the hood
When you run m87 <device> deploy docker-compose.yml:
- The CLI uploads a run specification to the server
- The device pulls the active deployment over its outbound connection
- Docker Compose commands execute on the device
- Results and logs are reported back to m87
If the device is offline, execution starts automatically once it reconnects.
Example: Docker Compose as an async service
Below is a simplified example of a Docker Compose–based service deployment.
jobs:
- id: app
type: service
enabled: true
workdir:
mode: ephemeral
files:
docker-compose.yml: |
services:
web:
image: nginx:alpine
ports:
- "80:80"
restart: always
steps:
- name: pull
run: docker compose pull
timeout: 15m
- name: up
run: docker compose up -d --remove-orphans
timeout: 10m
undo:
run: docker compose down --remove-orphans
timeout: 5m
stop:
steps:
- name: down
run: docker compose down --remove-orphans
timeout: 5m
This deployment:
- Pulls images on the device
- Starts the Compose stack
- Automatically tears it down when the job is stopped or removed
Updating a deployment
To update the deployment, re-run deploy with the modified compose file:
m87 <device> deploy docker-compose.yml
m87 replaces the existing run spec and applies the updated desired state.
Removing a deployment
To remove a Docker Compose job from the active deployment:
m87 <device> undeploy app
Any defined stop steps are executed during removal.
Rollback via deployment cloning
To roll back to a previous deployment state:
m87 <device> deployment clone <deployment-id> --active
This creates a new deployment from a known-good configuration and activates it immediately.
Async vs sync Docker workflows
| Workflow | Use when |
|---|---|
m87 <device> docker compose up | Interactive development |
Async deployment (deploy) | Production or unattended devices |
| Sync execution | Device is online now |
| Async deployment | Device may be offline or rebooting |
Both workflows can coexist and target the same device.
FAQ
What if the device is offline?
The deployment is applied automatically when the device reconnects.
What happens on reboot?
The device reconciles the active deployment after startup.
Where do files live on the device?
Files are placed in an ephemeral working directory unless configured otherwise.
Are retries handled automatically?
Yes. Timeouts and retries can be defined per step.