Decided to open a new category, “天书” (“Heavenly Book”), with the URL slug “黄石公”.
It will be used to store tutorials that are entirely AI-generated.
This is the first one.
Containerized Deployment of Mihomo Proxy Without Docker Hub
Applicable Scenarios
Works when the server can barely reach the public internet and Docker Hub is unavailable (or no images can be pulled at all). Podman, BaoTa Panel, and a directly installed Docker Engine all work.
Core Approach
- Use any Alpine image already on the server (e.g.
redis:alpine) as the CA certificate source during the build; the final image is based onscratch, so no additional image needs to be pulled. - All new files are kept under
/root/mihomo/, so nothing else on the server is touched. - Only the target container goes through the proxy; the host and other containers are unaffected.
Step 1: Prepare the Files
Download the following files on a PC that can access GitHub normally, then place them in the /root/mihomo/ directory on the server.
| File | Purpose | How to Get It |
|---|---|---|
mihomo (binary) | Proxy core | Download mihomo-linux-amd64-v*.gz from GitHub Releases, extract it, and rename it to mihomo |
geoip.metadb | GeoIP rule database | Download geoip.metadb from meta-rules-dat Releases, and put it in the data/ subdirectory |
config.yaml | Proxy configuration | See below |
Dockerfile | Image build | See below |
docker-compose.yml | Container orchestration | See below |
Final directory structure:
/root/mihomo/
├── mihomo # Binary
├── config.yaml # Proxy configuration
├── Dockerfile
├── docker-compose.yml
└── data/
└── geoip.metadb # GeoIP database
Step 2: config.yaml
Core idea: the subscription URL provides a complete configuration. Extract the DNS and rules sections from it and put them directly into config.yaml; proxy nodes are provided through proxy-providers pointing to the subscription URL, and mihomo automatically pulls updates every day.
Some parts vary by provider; fill in the corresponding fixed parts from the subscription URL supplied by your provider. AI can do this for you.
url:Enter your subscription address. The URL itself carries authentication (a random token in the path), so no additional authentication is required.
# Port and mode
port: 7890
socks-port: 7891
allow-lan: true
mode: Rule
log-level: info
external-controller: 0.0.0.0:9090
# DNS (extracted from the subscription response)
dns:
enable: true
# ... fill in the full content after pulling from the subscription URL
# Pull the latest nodes from the subscription URL every day
proxy-providers:
sub:
type: http
url: "https://your-subscription-url?clash=3"
interval: 86400
path: ./providers/sub.yaml
health-check:
enable: true
url: https://cp.cloudflare.com
interval: 300
# Automatically select the fastest node
proxy-groups:
- name: 🔰 Select Node
type: url-test
use: [sub]
url: https://www.gstatic.com/generate_204
interval: 300
# Rules (extracted from the subscription response)
rules:
- GEOIP,CN,DIRECT
- MATCH,🔰 Select Node
Step 3: Dockerfile
FROM redis:alpine AS certs
FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY mihomo /mihomo
ENTRYPOINT ["/mihomo"]
CMD ["-d", "/etc/mihomo"]
- The first stage,
FROM redis:alpine, uses an image already present on the server and only extracts the CA root certificate. - The second stage,
FROM scratch, is an empty image; the final result contains only the mihomo binary and the CA certificate — clean and free of redundancy. redis:alpineis referenced only as a build artifact. It is never run or modified, and can be replaced with any Alpine-based image already on the server.
Step 4: docker-compose.yml
services:
mihomo:
build: .
container_name: mihomo
restart: unless-stopped
volumes:
- ./config.yaml:/etc/mihomo/config.yaml:ro
- ./data:/etc/mihomo
ports:
- "7890:7890"
- "9090:9090"
build: .builds with the local Dockerfile, so no external image needs to be pulled.- Port
9090is the REST API (optional; used for hot reload or status queries).
Step 5: Build and Start
cd /root/mihomo
# Make sure the binary exists and is executable
chmod +x mihomo
# Build and start
docker compose build
docker compose up -d
Verify that the proxy works:
curl -x http://localhost:7890 https://www.google.com -o /dev/null -w "%{http_code}"
Returning 200 means success.
Step 6: Route Other Containers Through the Proxy
Using a blog backend container as an example, add these environment variables to its docker-compose.yml:
environment:
HTTP_PROXY: "http://172.17.0.1:7890"
HTTPS_PROXY: "http://172.17.0.1:7890"
NO_PROXY: "localhost,127.0.0.1"
172.17.0.1 is Docker's default bridge gateway. Containers use it to reach port 7890 mapped on the host. If the backend uses a custom network, confirm the actual gateway address with docker inspect <container-name> | grep Gateway.
After rebuilding the target container, enter it to verify:
docker exec <container-name> curl -x http://172.17.0.1:7890 https://www.google.com -o /dev/null -w "%{http_code}"
Returning 200 means it's done.
mx-space's OAuth
mx-space uses the better-auth package for OAuth authentication, but this package does not go through the container's proxy by default. So extra handling via JS injection is needed.
The Problem
Once the HTTP_PROXY environment variable is set, traditional tools like curl can use the proxy. But packages that use Node.js's native fetch(), such as better-auth and Google OAuth, still connect directly — because fetch() is built on undici, which does not read the HTTP_PROXY environment variable.
The Solution
Three steps: inject a script → declare the dependency → deploy.
1. Create proxy-patch.mjs
import { ProxyAgent, setGlobalDispatcher } from 'undici';
const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
if (proxy) {
setGlobalDispatcher(new ProxyAgent(proxy));
}
2. Add to the Container's docker-compose.yml
environment:
NODE_OPTIONS: "--import /app/proxy-patch.mjs"
# HTTP_PROXY and HTTPS_PROXY are already set
volumes:
- ./proxy-patch.mjs:/app/proxy-patch.mjs:ro
- ./undici:/app/node_modules/undici:ro # Only if undici is not preinstalled in the image
3. If undici Is Not Preinstalled in the Image
Download the undici npm package on your PC and place it in the container project directory:
# On your PC (requires npm)
npm pack undici --pack-destination .
tar -xzf undici-*.tgz
mv package undici
Then upload the undici/ directory to the server along with the rest of the container project.
The final structure looks something like this:
mx-core/
├── docker-compose.yml
├── proxy-patch.mjs
├── undici/ # undici 8.9.0 complete package
└── data/
Verification
Run this inside the container:
node -e "fetch('https://www.google.com').then(r => console.log(r.status))"
Returning 200 means the proxy is working.