Deployment Architecture and Scope
A Linux server usually does not need a graphical client. A more reliable setup runs the mihomo core as a standalone system service, with systemd handling startup, restarts, privilege reduction, and log collection. Keep configuration files in /etc/mihomo/, writable runtime data in /var/lib/mihomo/, and the binary at /usr/local/bin/mihomo. This separation keeps application updates, configuration changes, and cache cleanup from interfering with one another.
The examples in this guide use Ubuntu 24.04.2 LTS, Debian 12.10, systemd 255/252, and mihomo 1.19.10 as reference environments. Package commands may vary by distribution, but the approach to systemd services, TUN devices, and Linux capabilities is the same. With the original Clash core, some TUN, DNS, and rule-provider options may be unavailable, so check the version output first to confirm which core you are running.
uname -m
/usr/local/bin/mihomo -v
systemctl --version
ip -Version
Common results from uname -m include x86_64, aarch64, and armv7l; the downloaded binary must match the CPU architecture. The version output should clearly show the mihomo version and build details. If the command returns “Exec format error,” the architecture is usually wrong—not the file permissions.
| Path | Purpose | Recommended permissions |
|---|---|---|
/usr/local/bin/mihomo |
Fixed core executable | root:root 0755 |
/etc/mihomo/config.yaml |
Main configuration, proxy, and rule entry point | root:clash 0640 |
/var/lib/mihomo/ |
Geo data, rule cache, and runtime data | clash:clash 0750 |
/etc/systemd/system/mihomo.service |
systemd service unit | root:root 0644 |
Install the Binary and Create a Dedicated Service User
The following steps assume you already have a mihomo executable matching the current CPU architecture, temporarily saved as /tmp/mihomo. Install it at a fixed path first, then create a non-login system user. A dedicated user prevents the service from running as root indefinitely and limits access to subscription URLs, controller secrets, and proxy details in the configuration to the designated group.
sudo install -o root -g root -m 0755 /tmp/mihomo /usr/local/bin/mihomo
sudo useradd \
--system \
--home-dir /var/lib/mihomo \
--create-home \
--shell /usr/sbin/nologin \
clash
sudo install -d -o root -g clash -m 0750 /etc/mihomo
sudo install -d -o clash -g clash -m 0750 /var/lib/mihomo
/usr/local/bin/mihomo -v
If the system already has a user named clash, useradd will report that the user already exists. In that case, use id clash to check its home directory and groups. The service user needs neither a password nor membership in extra groups such as sudo or docker.
Write a Minimal Working Configuration
The configuration below sets up a local mixed proxy port, a REST controller, and TUN takeover. mixed-port: 7890 accepts both HTTP and SOCKS5 connections; the controller is restricted to port 9090 on the loopback address; DNS uses the unprivileged port 1053, avoiding the extra permission required to bind port 53.
mixed-port: 7890
bind-address: 127.0.0.1
allow-lan: false
mode: rule
log-level: info
ipv6: false
external-controller: 127.0.0.1:9090
secret: "Replace with a sufficiently long random controller secret"
profile:
store-selected: true
store-fake-ip: true
tun:
enable: true
stack: mixed
auto-route: true
auto-detect-interface: true
strict-route: true
dns-hijack:
- any:53
- tcp://any:53
dns:
enable: true
listen: 127.0.0.1:1053
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
default-nameserver:
- 223.5.5.5
- 1.1.1.1
nameserver:
- https://223.5.5.5/dns-query
- https://1.1.1.1/dns-query
proxies: []
proxy-groups:
- name: PROXY
type: select
proxies:
- DIRECT
rules:
- GEOIP,CN,DIRECT
- MATCH,PROXY
This configuration can verify that the process, ports, and TUN are working, but the PROXY group currently contains only DIRECT, so it will not proxy traffic in practice. For production use, convert an existing subscription into a mihomo-compatible configuration or reference a provider's compatible subscription through proxy-providers. Do not enter a web subscription URL as if it were a single proxy node.
After saving, tighten the permissions and run the syntax check as the service user. -d selects the runtime data directory, while -f explicitly selects the configuration file. Keeping them separate prevents Geo data and rule caches downloaded by mihomo from being written to /etc.
sudo chown root:clash /etc/mihomo/config.yaml
sudo chmod 0640 /etc/mihomo/config.yaml
sudo -u clash /usr/local/bin/mihomo \
-t \
-d /var/lib/mihomo \
-f /etc/mihomo/config.yaml
Configure the TUN Device and Capabilities
TUN mode requires the kernel to provide /dev/net/tun and the process to be allowed to modify routes, policy routing, and virtual network interfaces. The key capability is CAP_NET_ADMIN. The default ports 7890, 9090, and 1053 are all above 1024, so CAP_NET_BIND_SERVICE is not needed.
Check the TUN Module
test -c /dev/net/tun && echo "TUN device ready"
ls -l /dev/net/tun
sudo modprobe tun
cat /sys/class/misc/tun/dev
Under normal conditions, the final command outputs 10:200. If modprobe tun succeeds but the device is still missing, check whether the current kernel was built without TUN support. In a container, the host must also expose the character device 10:200; creating a same-named file inside the container does not provide TUN functionality.
If the module must be loaded explicitly at every boot, add it to the modules-load configuration:
echo tun | sudo tee /etc/modules-load.d/tun.conf
sudo systemctl restart systemd-modules-load.service
Grant Capabilities Through systemd First
Linux commonly handles this in one of two ways: assigning a file capability to the binary or granting the capability when systemd starts the process. The service-based approach is preferable here. Replacing the binary will not remove the capability, and the permitted scope remains visible in the service unit.
Create /etc/systemd/system/mihomo.service:
[Unit]
Description=mihomo proxy service
Documentation=https://wiki.metacubex.one/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=clash
Group=clash
WorkingDirectory=/var/lib/mihomo
ExecStartPre=/usr/local/bin/mihomo -t -d /var/lib/mihomo -f /etc/mihomo/config.yaml
ExecStart=/usr/local/bin/mihomo -d /var/lib/mihomo -f /etc/mihomo/config.yaml
Restart=on-failure
RestartSec=3s
TimeoutStopSec=15s
LimitNOFILE=1048576
AmbientCapabilities=CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_ADMIN
NoNewPrivileges=true
DevicePolicy=closed
DeviceAllow=/dev/net/tun rw
PrivateDevices=false
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mihomo
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
[Install]
WantedBy=multi-user.target
AmbientCapabilities passes CAP_NET_ADMIN to the non-root service process, while CapabilityBoundingSet prevents it from acquiring capabilities outside the list. DevicePolicy=closed combined with DeviceAllow=/dev/net/tun rw exposes only the TUN character device required at runtime. ProtectSystem=strict makes system directories read-only, while ReadWritePaths explicitly allows mihomo to write to its state directory.
If a distribution or older systemd release does not support one of these hardening directives, the logs will identify the unknown field. Adjust that line for the installed systemd version rather than reverting to running the service as root just to bypass one error.
File Capabilities as a Fallback
Without systemd management, you can grant the binary CAP_NET_ADMIN, but the attribute disappears when a new binary is installed over it. With this approach, do not also configure systemd's AmbientCapabilities.
sudo setcap cap_net_admin=+ep /usr/local/bin/mihomo
getcap /usr/local/bin/mihomo
# Revoke when needed
sudo setcap -r /usr/local/bin/mihomo
Some filesystems mounted with nosuid ignore file capabilities, and some container runtimes filter capabilities as well. When you see Operation not permitted, check the mount options, the container's capability set, and the host's device mapping together.
Start the Service, Enable Boot Startup, and Verify Runtime
After writing the service unit, have systemd reload its configuration before starting the service and adding it to the boot target. enable --now creates the startup link and starts the service in one step.
sudo systemctl daemon-reload
sudo systemctl enable --now mihomo.service
sudo systemctl status mihomo.service --no-pager
A healthy status should show Active: active (running), with the main process owned by clash. In the reference environment, a configuration with about 620 rules and 2 proxy providers takes roughly 0.8 to 1.2 seconds to cold-start and uses about 45 to 85 MB of resident memory after stabilizing. Memory use will vary with the number of rule sets, Geo data, and connections.
Check Listening Ports and Interfaces
sudo ss -lntup | grep -E ':(7890|9090|1053)\b'
ip tuntap show
ip rule show
ip route show table all | grep -E '198\.18\.|default'
systemctl show mihomo.service -p User -p MainPID
The listening addresses should match the configuration: 7890, 9090, and 1053 should all listen only on 127.0.0.1. The TUN interface created by mihomo can vary by version and configuration, so do not rely on a fixed name alone. Checking ip tuntap show, policy routes, and service logs together is more reliable.
Verify the HTTP Proxy and TUN Routing
First test the proxy entry point by explicitly specifying the mixed port, then test a normal connection without proxy environment variables. Because the example policy group currently points to DIRECT, the goal here is to confirm that the request passes through mihomo, DNS does not time out, and routing does not form a loop.
curl --proxy http://127.0.0.1:7890 \
--connect-timeout 5 \
https://www.example.com/ -I
env -u http_proxy -u https_proxy -u all_proxy \
curl --connect-timeout 5 \
https://www.example.com/ -I
sudo journalctl -u mihomo.service -n 50 --no-pager
After importing actual proxy nodes, temporarily set log-level to debug to see which rule matches the destination and which policy group is selected. Switch it back to info after troubleshooting; otherwise, long-running services will generate a large volume of logs.
Update the Configuration and Reload Safely
Do not restart immediately after editing YAML. Run the test command first to verify indentation, rule syntax, policy-group references, and provider paths, then let systemd restart the service. YAML uses spaces for indentation; tab characters cause parsing failures. Policy-group names referenced by rules must also match proxy-groups exactly.
sudo -u clash /usr/local/bin/mihomo \
-t \
-d /var/lib/mihomo \
-f /etc/mihomo/config.yaml
sudo systemctl restart mihomo.service
sudo systemctl status mihomo.service --no-pager
sudo journalctl -u mihomo.service --since "2 minutes ago" --no-pager
If the configuration comes from a subscription, download it to a temporary file first, test its syntax, and then atomically replace the active configuration. This ensures systemd reads only a complete file after restarting instead of encountering a partially downloaded YAML file.
sudo install -o root -g clash -m 0640 \
/tmp/config.yaml \
/etc/mihomo/config.yaml.new
sudo -u clash /usr/local/bin/mihomo \
-t \
-d /var/lib/mihomo \
-f /etc/mihomo/config.yaml.new
sudo mv /etc/mihomo/config.yaml.new /etc/mihomo/config.yaml
sudo systemctl restart mihomo.service
mihomo's external controller also supports configuration reloads, but remote automation scripts must handle the controller secret, listening scope, and rollback on failure carefully. For a single-server setup, testing first and then running systemctl restart is clearer; the interruption is usually only a few seconds.
Log Diagnosis and Common Failures
The Service Keeps Restarting
Start by checking the logs and exit code for the current startup cycle. Because the service uses Restart=on-failure, a syntax error can trigger repeated restarts. During troubleshooting, stop the service first and run the configuration test directly.
sudo journalctl -u mihomo.service -b --no-pager
sudo systemctl show mihomo.service \
-p ExecMainCode \
-p ExecMainStatus \
-p NRestarts
sudo systemctl stop mihomo.service
sudo -u clash /usr/local/bin/mihomo \
-t \
-d /var/lib/mihomo \
-f /etc/mihomo/config.yaml
Common errors include inconsistent YAML indentation, references to missing policy groups, a configuration file that the clash user cannot read, and a provider directory that is not writable. Use namei -l /etc/mihomo/config.yaml to inspect permissions on each directory component.
“permission denied” When Creating the TUN Device
Check the TUN device, service capabilities, and systemd device policy in order. It is not enough to confirm that /dev/net/tun exists; without CAP_NET_ADMIN, the process still cannot create the interface or write policy routes.
ls -l /dev/net/tun
systemctl show mihomo.service \
-p AmbientCapabilities \
-p CapabilityBoundingSet \
-p DevicePolicy
sudo journalctl -u mihomo.service -n 100 --no-pager | \
grep -Ei 'tun|permission|operation not permitted'
If the service runs in LXC, Docker, or another container, the host must expose /dev/net/tun and grant NET_ADMIN. If the cloud server's kernel lacks TUN support, container-side settings cannot add that kernel capability.
DNS Times Out After Enabling TUN
First confirm that port 1053 is listening, then check whether the upstream DNS servers are reachable. If systemd-resolved is also running, it usually occupies 127.0.0.53:53, which does not directly conflict with mihomo listening on 127.0.0.1:1053. The real concern is whether TUN DNS hijacking sends mihomo's own upstream queries back into TUN, creating a loop.
sudo ss -lnup | grep ':1053'
resolvectl status
dig @127.0.0.1 -p 1053 www.example.com
sudo journalctl -u mihomo.service -n 100 --no-pager | \
grep -Ei 'dns|timeout|loop'
Follow this order: verify that the default egress interface is detected correctly, confirm that the nameserver is reachable, and then check whether a rule is incorrectly intercepting upstream DNS traffic. On multi-homed servers, explicitly select the interface in the TUN configuration to prevent automatic detection from choosing a Docker, WireGuard, or temporary VPN interface.
Works Locally, but LAN Devices Cannot Connect
The example deliberately uses bind-address: 127.0.0.1 and allow-lan: false, so other devices cannot access 7890. If you need to provide a LAN proxy, change the listening address to the server's private network address and enable LAN access, while allowing only trusted subnets through the firewall.
# Example only: allow 192.168.10.0/24 to access TCP 7890
sudo ufw allow from 192.168.10.0/24 to any port 7890 proto tcp
sudo ufw status numbered
Keep the external controller on 127.0.0.1 and access it through SSH port forwarding. If it must listen on a private network address, set a random secret and restrict the source addresses at minimum. Never expose the control interface directly to the public internet.
Operations and Maintenance Checklist
- The binary architecture matches
uname -m; after upgrading, runmihomo -vagain. - The main configuration is owned by
root:clash, has permissions0640, and the service user owns the runtime directory. - Run
mihomo -tbefore replacing the configuration; restart the service only after the test passes. - TUN mode requires only
CAP_NET_ADMIN; the default ports do not require low-port binding capability. /dev/net/tunexists; in containers, both host device mapping and capability grants are configured.- The controller listens on
127.0.0.1:9090with a secret configured; remote administration uses SSH forwarding. - Keep logging at
info; switch todebugonly during short troubleshooting sessions. - Prepare an automatic stop job before changing TUN routes to prevent a default-route change from breaking a remote SSH session.
After these steps, systemd starts mihomo once the network is ready, retries automatically after a 3-second delay if it exits unexpectedly, and allows 15 seconds for shutdown. Configuration, the binary, and runtime data remain separately managed, giving you clear boundaries for updating the core, changing subscriptions, or migrating the server.