Tutorial ~17 min read

Deploy Mihomo in Docker: Compose, Volume Mounts, and Port Mapping (2026)

Homelab operators, small-office admins, and NAS owners often want the same Mihomo engine you know from desktop Clash.Meta clients—except running headlessly on Linux hardware that already hosts Plex, file sync, or CI runners. Docker plus a checked-in docker-compose file gives you reproducible deploys: pull an image, mount a directory for config.yaml, publish the mixed listener to the host, and point phones, laptops, or other containers at a single container proxy endpoint. This guide is the container-shaped companion to our Linux Mihomo TUN and systemd walkthrough; here the emphasis is on volume mounts, port mapping, bridge networking, and how LAN clients reach the published ports without mystery firewall gaps.

Clash Editorial Team Docker · Mihomo · Compose · Volumes · Ports

Why run Mihomo in Docker on a server or NAS?

Bare-metal systemd units are excellent when you want TUN on the host routing table and tight integration with NetworkManager—see the linked Linux guide for that story. Containers shine when you already standardize workloads with Docker, when your vendor ships a NAS appliance with a container UI, or when you want upgrades to be “replace the image tag and restart” instead of chasing distribution packages. A single compose file documents the exact image digest policy, bind paths, published ports, restart behavior, and optional resource limits so you can recreate the same Mihomo footprint on a second machine after a hardware failure.

The tradeoff is mental-model overhead: listeners live inside a network namespace unless you choose host mode, file permissions map through UID/GID on bind mounts, and anything involving kernel tunnels needs extra capabilities. For many readers the sweet spot is bridge networking with a published mixed-port: the host becomes a stable rendezvous IP while the core still behaves like ordinary Clash YAML inside the container. If you later need full-host capture, you can revisit TUN with privileged or fine-grained caps—but do not start there on a machine you barely understand.

Mental model: what port mapping actually does

When you declare ports: in Compose, Docker’s userland proxy forwards traffic from a host TCP/UDP tuple into the container’s address space. Mihomo’s mixed-port speaks both HTTP proxy and SOCKS on the same numeric port; publishing 7890:7890 means “whatever listens on TCP 7890 inside the container is reachable on the host’s 7890 as well.” Changing the left side to 18890:7890 keeps the internal port fixed while avoiding collisions with another daemon on the host—a common pattern on busy NAS boxes.

UDP matters for some transports and for DNS-forwarding experiments; if your profile relies on UDP-associated features, verify both protocols are forwarded when you map ports. Firewall rules on the host still apply after Docker publishes the port, so a successful docker compose ps line does not guarantee WAN exposure—you must consciously decide whether 7890 should be reachable from the internet or only from RFC1918 LAN ranges.

Volume mounts: where configuration should live

Mihomo reads its primary YAML from the directory you pass with -d or the equivalent environment convention your image documents. A bind mount such as ./mihomo:/root/.config/mihomo stores files directly on the host filesystem path you control, which makes editor workflows, git tracking of sanitized templates, and backup jobs trivial. A named volume delegates storage to Docker’s volume driver—convenient on desktops, sometimes awkward on NAS devices where you specifically want the array path for snapshots.

Regardless of style, treat the mounted directory as the single source of truth for config.yaml, rule providers, GeoIP databases, and cached subscriptions. Avoid baking secrets into the image layers; mount them at runtime so rebuilding the container never duplicates keys into intermediate build steps. If your image writes state files (logs, caches), ensure the mount target directory is writable by the container user or adjust ownership with chown on the host once.

Backup hint: snapshot the mounted host folder before image upgrades—rollback is then “stop container, restore folder, start old tag,” not archaeology inside anonymous volumes.

1Baseline docker-compose.yml (bridge + published mixed port)

The following fragment is illustrative: replace the image reference with the upstream-maintained tag you trust, align the mount target with that image’s documented config path, and tune published ports to match your config.yaml. Many community images default to listening on 0.0.0.0 inside the container; pairing that with explicit host publishing is what turns the stack into a container proxy for other devices.

services:
  mihomo:
    image: metacubex/mihomo:latest
    container_name: mihomo
    restart: unless-stopped
    volumes:
      - ./mihomo-config:/root/.config/mihomo
    ports:
      - "7890:7890/tcp"
      - "7890:7890/udp"
    # Keep API on loopback inside container unless you know why not:
    environment:
      - TZ=UTC

Your real config.yaml should set mixed-port: 7890 (or another consistent value) and should bind external-controller thoughtfully—covered next. If the container exits immediately, check logs with docker compose logs -f; malformed YAML and permission denied on the mount are the usual first-run culprits.

2external-controller, LAN dashboards, and least exposure

The REST API is powerful: it can reload profiles, inspect connections, and flip proxy groups. Binding it to 0.0.0.0 inside the container and publishing another host port makes phone dashboards convenient—and widens your attack surface to every client that can route to that port. Safer defaults keep the controller on 127.0.0.1 inside the container and reach it through SSH port forwarding or a reverse proxy with authentication when you truly need remote access.

If you deliberately publish the API for a trusted LAN, put it on a non-obvious host port, firewall by source IP, and set a strong secret in the YAML. Treat the controller like an admin socket, not a guest Wi-Fi feature. For DNS-heavy profiles, align resolver behavior with our Meta core DNS leak prevention guide so the same policy you test on desktop survives inside the container.

Warning: exposing Mihomo’s API without network ACLs is equivalent to handing strangers a remote-control knob for egress traffic.

3Pointing LAN clients at the host’s published port

Once 7890 (or your chosen host port) answers on the server’s LAN IP, configure other machines to use an HTTP or SOCKS proxy aimed at 192.168.x.y:7890. Browsers and operating system proxy settings work as they would against any local Clash listener; the only difference is that the hop is remote. For mobile devices on the same VLAN, the same IP:port pair applies—verify AP isolation is disabled on guest SSIDs if you expect cross-device access.

Some applications refuse plain HTTP proxies for TLS-heavy stacks; SOCKS5 often behaves better for arbitrary TCP. Because Mihomo’s mixed port negotiates both, you can standardize on one number in household documentation. When a stubborn app ignores system proxy settings, you are back to either per-app SOCKS configuration or host-level TUN—Docker does not magically fix applications that bypass the proxy chain.

4Other Docker workloads: bridge DNS names and gateway IPs

Containers on the same user-defined bridge can reach each other by service name. A sidecar scraper might use http://mihomo:7890 as its outbound proxy if both services share a compose project network. For containers spawned outside that compose file, use the host’s docker bridge gateway IP or run with extra_hosts patterns your platform supports—details vary slightly between Linux, macOS Docker Desktop, and rootless Podman, so validate with curl from inside a throwaway alpine container before you bake assumptions into production jobs.

Host networking: when it helps and when it hurts

Linux network_mode: host removes NAT port publishing because the container shares the host stack directly. That can simplify QUIC-heavy scenarios or odd port ranges at the cost of portability: compose files stop expressing explicit ports: mappings, and two Mihomo instances cannot coexist on the same host interface. NAS platforms sometimes hide or restrict host networking in their GUI—read vendor notes before you rely on it.

TUN inside Docker: advanced, not mandatory

Full transparent proxying comparable to desktop TUN typically requires CAP_NET_ADMIN, access to /dev/net/tun, and sometimes privileged: true. That is workable for dedicated lab routers but rarely the first milestone for “run Mihomo beside Plex.” If you need host-wide capture, compare complexity against running Mihomo directly under systemd on the same machine—the linked Linux article walks that path with explicit capability lines you can reason about without Docker’s extra layer.

Subscriptions, rule providers, and Subconverter

Containerized cores still consume ordinary Clash YAML. If your provider hands you non-Clash links, convert them with tooling you control; our Subconverter guide for Clash YAML walks through safer patterns than pasting secrets into untrusted web forms. After conversion, drop the resulting profile into the mounted directory and reload through the API or restart the container depending on how your image handles hot reload.

Operations: updates, logs, and healthchecks

Pin image tags in serious deployments—:latest is convenient until a breaking change surprises you at 2 a.m. Update with docker compose pull followed by docker compose up -d, and watch logs during the first minutes. Compose’s restart: unless-stopped covers crude crashes; optional HTTP healthchecks against the external controller can accelerate orchestrator decisions if you later move to Kubernetes-style platforms.

Disk growth usually comes from logs and downloaded rule providers, not from the binary itself. Rotate log files if your image writes them to the mount, and prune obsolete provider artifacts when you change rule sets. For configuration semantics shared across GUI and headless installs, keep our configuration documentation open alongside container logs when you debug mismatches between expected and actual rule hits.

Verification checklist

  1. docker compose ps shows the service healthy and not restart-looping.
  2. From the host, curl -x http://127.0.0.1:7890 -I https://example.com returns plausible headers.
  3. From a LAN client, the same test against the server’s LAN IP succeeds when firewall rules allow it.
  4. DNS resolution paths match your policy expectations (cross-check with the DNS article).
  5. Stopping the stack removes the published listener—no zombie iptables surprises from older experiments.

Troubleshooting quick hits

Permission denied on mount: UID inside the container may not match host ownership—adjust with chown or map user namespaces. Port already allocated: another service owns the host port; change the left side of the mapping. Works on host, fails from LAN: OS firewall, Docker’s binding to localhost only, or upstream router guest isolation. TLS errors only in container: time skew—set TZ and verify NTP on the host. Each symptom maps to a boring infrastructure fix; resist the urge to randomly toggle tun when the issue is simply “published port not reachable.”

Upstream binaries versus graphical clients

Container images track upstream Mihomo releases; checksums and release notes belong on the project’s GitHub pages alongside security advisories. For everyday graphical installs on desktops and phones, prefer our official Clash download page so users receive maintained client bundles rather than mixing “random DockerHub tag” culture with end-user expectations. The YAML vocabulary stays parallel: groups, rules, and DNS stanzas you learn in a GUI transfer directly to the file the container mounts.

Summary

A solid Docker deployment is mostly boring infrastructure: an explicit compose file, a persistent directory for config.yaml, carefully chosen port mapping for the mixed listener, and a conservative stance on the external controller. Compared with one-off shell scripts, that stack is easier to audit, duplicate on a cold spare, and explain to housemates who just need “the proxy IP and port on the fridge whiteboard.” When you want the same engine on your laptop with buttons and tray icons instead of YAML-only workflows, the desktop path is a download away—

Download Clash for free and experience the difference

Clash for desktop and mobile Meta core

Running Mihomo in Docker on a NAS or server is ideal for headless hops. When you want the same policy on a GUI with one-click updates, pair this compose workflow with official clients from our download hub.

Official installers

Windows, macOS, Linux, Android, iOS builds in one place

Same YAML vocabulary

Move profiles between container cores and GUI clients

DNS depth

FakeIP and resolver guides for Meta-class cores

Docs + Linux TUN

systemd path when Docker TUN is overkill

Previous & Next

Related Reading

Docker + Mihomo on NAS?

Compose, volumes, and port maps cover the server hop; grab a polished Clash client from our download page when you want the same profile on laptops and phones.

Download Free Client