← Back to Guides

    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:

    1. Context sync: m87 syncs your local working directory to the device (only files referenced by the compose file and Dockerfiles)
    2. Image resolution: Images are pulled from registries on the device, or built from local Dockerfiles on the device
    3. 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.


    Next