Deploy Docker Compose to edge devices (without SSH)
You can deploy Docker Compose stacks to edge devices behind NAT without SSH or VPN. With m87 (make87's command line + device runtime), you run docker and docker compose commands on your laptop and they execute on the remote device — your local working directory is synced automatically when needed, images are pulled or built on-device, and containers run remotely. No file copying, no inbound ports, no CI/CD pipelines required.
The problem
Deploying containers to edge devices typically requires SSH access with exposed ports, CI/CD pipelines with runners that can reach each device, or physical/VPN access for manual updates. All of these assume you control the network — they break down when devices sit behind NAT, CGNAT, or firewalls that block inbound connections.
The solution: Docker API passthrough
m87 proxies Docker API calls over the device's outbound QUIC tunnel. When you run m87 <device> docker, the command executes against the remote Docker daemon as if you had a direct socket connection:
Your laptop Edge device
┌─────────────┐ ┌─────────────┐
│ docker │ │ Docker │
│ compose │──── QUIC/443 (via make87 server) ────>│ daemon │
│ (via m87) │ │ │
└─────────────┘ └─────────────┘
Your local docker-compose.yml and build context are synced to the device automatically. Images are pulled from registries or built on the device. The result: you work locally, containers run remotely.
Quick start
1) Authenticate and verify device reachability
m87 login
m87 devices list
2) Deploy your compose stack
From your project directory (where docker-compose.yml lives):
m87 rpi docker compose up -d
That's it. m87 syncs your compose file and build context to the device, then runs docker compose up -d against the remote daemon.
3) Verify the deployment
m87 rpi docker ps
How it works
When you run m87 <device> docker compose up -d:
- Context sync: m87 syncs your local working directory to the device (only files referenced by the compose file and Dockerfiles)
- Image resolution: Images are pulled from registries on the device, or built from local Dockerfiles on the device
- Container execution: Containers start on the device using the remote Docker daemon
This is the same workflow as setting DOCKER_HOST to a remote daemon, but without managing SSH tunnels or exposing the Docker socket.
Common workflows
Deploy from a project directory
cd ~/projects/my-app
m87 rpi docker compose up -d
Pull latest images and redeploy
m87 rpi docker compose pull
m87 rpi docker compose up -d
Build and deploy (when using local Dockerfiles)
m87 rpi docker compose build
m87 rpi docker compose up -d
Or in one command:
m87 rpi docker compose up -d --build
Deploy a specific compose file
m87 rpi docker compose -f docker-compose.prod.yml up -d
Monitoring your deployment
View running containers
m87 rpi docker ps
Stream logs
m87 rpi docker compose logs -f
Stream logs for a specific service
m87 rpi docker compose logs -f web
Check system resources
m87 rpi stats
Execute commands in a running container
m87 rpi docker exec -it mycontainer sh
Updating a deployment
When you change your compose file, Dockerfiles, or application code:
m87 rpi docker compose up -d --build
m87 syncs the updated files, rebuilds images if needed, and recreates containers with changes.
For image-only updates (no local build):
m87 rpi docker compose pull
m87 rpi docker compose up -d
Alternatives (and tradeoffs)
SSH + rsync
Pros:
- Standard tooling (OpenSSH, rsync) with no additional dependencies on target
- Scriptable with existing shell knowledge
Cons:
- Requires inbound port 22 (or custom) exposed per device
- SSH key rotation and revocation across fleet
- NAT/CGNAT requires a bastion host with static IP or VPN tunnel
- Manual file sync + remote exec workflow
CI/CD pipelines (GitHub Actions, GitLab CI)
Pros:
- Git-triggered automation with audit trail
- Rollback via git history
Cons:
- Runner-to-device network path (self-hosted runners or VPN tunnels)
- SSH keys/credentials stored as CI secrets
- Pipeline YAML complexity for multi-device targeting
Kubernetes / k3s
Pros:
- Declarative state with built-in rolling updates, health checks, and service discovery
Cons:
- ~512MB RAM minimum for k3s server node
- etcd/SQLite cluster state management and backup
- kubeconfig distribution and certificate rotation
- Devices must reach API server (control plane connectivity)
FAQ
Do I need to open inbound ports?
No. The device maintains a persistent outbound QUIC connection (UDP/443); you don't need inbound firewall rules.
Does this work behind CGNAT?
Yes. The device initiates outbound UDP/443 and maintains the connection with keepalives — no inbound NAT mappings required.
Can I deploy to multiple devices?
Yes. Run the same commands against different device names:
m87 rpi-01 docker compose up -d
m87 rpi-02 docker compose up -d
For fleet-wide deployments, script the commands or use make87's fleet features.
What if my compose file references local build contexts?
They're synced automatically. When you run docker compose up --build, m87 syncs your Dockerfiles and source code to the device, then builds the images there.
Where do images get pulled from?
From the device. The remote Docker daemon pulls images from Docker Hub, GHCR, or whatever registries you've configured. If you use private registries, configure Docker on the device to authenticate with them.
Is this secure?
Yes. All traffic flows through TLS 1.3-encrypted QUIC streams with API key authentication. The device never exposes inbound ports, and the Docker socket is never exposed to the network.
How is this different from setting DOCKER_HOST?
It's the same concept — your local Docker commands run against a remote daemon. The difference is m87 handles the transport (QUIC tunnel through NAT) and context syncing automatically, without you managing SSH tunnels or socket exposure.