Start your 3-day free trial
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.


A subconverter self hosted on your own computer converts a proxy subscription into another client format without sending the subscription link to a public conversion website. This guide runs the pinned tindy2013/subconverter:0.9.0 Docker image, publishes it on 127.0.0.1 only, keeps the link out of shell history and container logs, and checks the output before any client imports it.[1][2]
Key Takeaways
- A subscription URL is a credential; a public converter receives it in full.
- Publish the container on
127.0.0.1, because Docker publishes to all host addresses by default.- Replace the default API token and disable container logs, which record the fetched URL at the default level.
- Version
0.9.0parses ss, ssr, vmess, trojan, socks, and HTTP links; it has no VLESS link parser.- Compare node count, TLS, transport, and UDP fields before trusting the converted file.
This procedure is based on the official sources listed below; a complete end-to-end test has not been performed. It covers one computer you control, not a public or shared conversion service. Use the self-hosting scope and recovery checklist to decide whether a local tool fits your setup.
The converter's interface takes the subscription address as the url query parameter of /sub, together with the target format and an optional external configuration.[1] Whoever runs that endpoint therefore receives the complete link, can fetch it again later, and sees every server, port, and key it returns. Logs, analytics, and caching layers on that site can keep copies you never learn about.
| Item in the request | Who learns it with a public site | Who learns it with a local container |
|---|---|---|
| Subscription URL and its token | Site operator and its logging | Only your computer, plus the subscription provider |
| Node addresses, ports, and keys | Site operator, after fetching the link | Only your computer |
| Your IP address when converting | Site operator; the provider sees the site's address | Subscription provider only |
| Converted output file | Site operator, during generation | Only your computer |
That is why subscription links should be stored and revoked like passwords. If you have already pasted a link into a public converter, ask the provider to reset it before using this guide. And if editing converted configurations for many nodes is more upkeep than your task needs, weigh running your own servers against a managed VPN first.
The latest tagged release is v0.9.0, and Docker Hub carries a matching 0.9.0 image tag; the latest tag follows newer builds of the main branch.[2][3] Pin 0.9.0 so the code you reviewed is the code you run, and re-check the repository for newer tags before each use.
Know the parser's limits before converting. At v0.9.0, the link parser recognizes ssr://, vmess://, ss://, trojan://, socks and Telegram-style HTTP links, and Netch://; it has no branch for vless://, Hysteria2, or TUIC links.[4] Nodes in unsupported formats will be missing from the output rather than converted. The documented targets include clash, surge, quanx, loon, and v2ray, and the v0.9.0 request handler also contains a singbox target that the README table does not list.[1][5]
Create a directory that only your user can read, and generate a new access token instead of keeping the default. The example preference file ships with api_access_token=password, and the program also reads API_MODE and API_TOKEN from the environment.[6][7]
mkdir -m 700 -p ~/subconv && cd ~/subconv
umask 077
printf 'API_MODE=true\nAPI_TOKEN=%s\n' "$(openssl rand -hex 24)" > subconv.env
API_MODE=true stops the service from loading local subscriptions or serving local files directly.[6] The subconverter API token protects the configuration-update endpoint, which the Docker README shows being called with a token parameter.[3] Keeping both in an environment file avoids putting the token on the command line, where shell history and process listings can expose it.
Docker publishes container ports on all host addresses (0.0.0.0 and [::]) unless you name one, which exposes the service to your network.[8] Bind it to loopback explicitly:
docker run -d --name subconv-local --restart unless-stopped \
--log-driver none --memory 256m \
--cap-drop ALL --security-opt no-new-privileges \
--env-file ./subconv.env \
-p 127.0.0.1:25500:25500 \
tindy2013/subconverter:0.9.0
Check it with curl -fsS http://127.0.0.1:25500/version; the Docker README says the output should name the subconverter backend and version.[3] Then confirm with sudo ss -lntp | grep 25500 that the host listens only on 127.0.0.1:25500; this subconverter Docker localhost binding is what keeps other devices out.
On Docker Engine releases older than 28.0.0, hosts on the same layer-2 network segment can reach ports published to localhost.[8] Check docker version and upgrade before relying on the loopback binding on a shared network.
Read the link from a hidden prompt into a private file, then let curl URL-encode the file's contents:
read -rs SUB_URL && printf '%s' "$SUB_URL" > sub.txt && unset SUB_URL
curl -fsS -G http://127.0.0.1:25500/sub \
--data-urlencode target=clash \
--data-urlencode url@sub.txt \
-o converted.yaml
rm sub.txt
The container fetches the subscription itself. In the example preference file, proxy_subscription=NONE means that fetch goes out directly rather than through a configured proxy.[6] Your subscription provider therefore sees your computer's address, exactly as it would when your client updates the link.
At v0.9.0, the handler writes an info-level log line that includes each subscription URL it fetches.[5] With Docker's default logging, that line would sit in docker logs output. The none logging driver keeps no container logs, so docker logs returns nothing.[9] If you need logs while debugging, run a temporary container with logging enabled and delete it afterward.
A successful HTTP response does not mean every node converted correctly. Open converted.yaml in a local editor and check it against what your provider documents for each node.
| Field | What to check | Stop if |
|---|---|---|
| Node count | Same number as the source, minus nodes you meant to drop | Nodes are missing without explanation |
| Protocol type | Matches the source node | A node changed protocol or vanished |
| Server and port | Identical to the source | Any address or port differs |
| TLS and server name | TLS still on, same SNI, no skip-cert-verify: true added | Certificate checks were disabled |
| Transport and path | Same WebSocket or other transport path and host | Path or host is empty or different |
| UDP flag | Matches what the server supports | UDP is enabled for a server that does not offer it |
The example preference file sets exclude_remarks=(到期|剩余流量|时间|官网|产品|平台), which drops nodes whose names contain any of those words.[6] It targets the information-only entries some providers add, but a real node whose name matches would disappear too. The default configuration also adds proxy groups and rule sets, so review the routing rules as well as the proxy entries.
Treat converted.yaml as a secret: it contains every credential from the subscription. Import it with the precautions in the guide to importing a configuration without leaking credentials, then delete the local copy if the client keeps its own.
Only processes on your computer can reach a loopback-published port, but that includes every local user and app. Run this on a single-user machine, and stop the container when you finish converting. The same reasoning applies to other local control interfaces, such as a Mihomo controller API: loopback limits who can connect, and a strong secret limits what a connected process can do.
Keep subconv.env and the working directory at mode 0700 or 0600. Do not add a reverse proxy, port forward, or tunnel to make the converter reachable from other devices; that turns a private tool back into a conversion service holding everyone's links. If a client must refresh automatically, convert on a schedule on the same machine and let the client read the local file.
To update, check the repository for a new release, read its notes, and pull the new pinned tag. Stop and remove the old container with docker rm -f subconv-local, start the new tag with the same command, and repeat the /version, listener, and output checks.
To remove everything, run docker rm -f subconv-local, delete the image with docker image rm tindy2013/subconverter:0.9.0, and remove ~/subconv. If the subscription link ever passed through a shared or public system, reset it with your provider afterward.
127.0.0.1, replace the token, and disable logs that would record the link.0.9.0 cannot convert VLESS, Hysteria2, or TUIC links, so check the output for missing nodes.It removes the public site from the path, but your computer, other local users, and the subscription provider still matter. Protect the working files and logs as well.
A fixed tag lets you review release notes and know which parser and defaults you run. The latest tag can change underneath you between conversions.
The version 0.9.0 link parser has no branch for vless links, so those entries are skipped. Use a client that reads the original format directly.
Yes. Any local process can reach the port, and the default value is the publicly documented word password, which anyone could guess immediately.
At the default info level, version 0.9.0 logs each fetched subscription URL. Docker's default logging would store that line, so this guide disables container logs.
Not with this setup. Publishing it to the network would expose every user's subscription link to the service and anyone who can reach it.
Yes. It contains every server address, port, and key from the subscription, so store it privately and delete copies the client no longer needs.
Disclaimer: This article is for general informational purposes only and does not constitute legal, technical, or other professional advice. We make no guarantees regarding the accuracy, completeness, or timeliness of the content.
Sources:
Sources checked 14 October 2026.
Related Articles:
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.





