Advanced Xray deployments become difficult when one large JSON file contains every inbound, outbound, DNS rule, routing exception, and credential. A configuration may work today and still be hard to review tomorrow: a single misplaced rule can send private traffic through the proxy, a DNS fallback can expose queries, and a copied UUID or private key can spread into logs and backups. A modular design treats the configuration as a set of independently reviewed building blocks that are assembled into one runtime file.
This guide presents a reusable approach for Xray JSON configuration in 2026. It focuses on Xray-core used directly or through clients such as v2rayN, while keeping the concepts portable to a gateway deployment. The examples cover file organization, inbound and outbound tags, DNS handling, ordered routing, secret management, validation, and troubleshooting. The goal is not to provide one universal profile, but to show how to construct a predictable configuration whose behavior can be tested layer by layer.
You will learn how to split an Xray configuration into maintainable modules, assemble it without assuming unsupported native includes, route private, direct, blocked, and proxied traffic in a safe order, reduce DNS leaks, keep credentials outside reusable templates, and validate the final JSON before restarting a production core.
Design the configuration as a contract
An Xray JSON file is not merely a collection of settings. Each section forms a contract with the others. An inbound exposes a local or transparent-proxy entry point, an outbound defines where traffic leaves, DNS determines how names are resolved, and routing decides which outbound receives each connection. Tags are the names that connect these sections. If an inbound tag is renamed without updating a routing rule, the file may still be valid JSON while the intended policy silently stops matching.
A useful contract begins with stable names. For example, use socks-in and http-in for local listeners, proxy-out for the primary proxy, direct-out for ordinary access, block-out for rejected traffic, and dns-out only when DNS requires a separately controlled path. Avoid tags such as node1 or test2 in policy rules. A server can change while the role remains the same; role-based tags make the routing layer independent from the selected endpoint.
The word “modular” needs a precise meaning. Xray reads the JSON document supplied at startup. It does not universally merge every JSON file found in a directory, and a directory containing 10-inbounds.json and 40-routing.json is not automatically a valid Xray configuration. You can keep source fragments separately and assemble them with a deployment script, configuration generator, or client-specific mechanism, but the final result passed to Xray must obey the core’s expected schema. This distinction prevents a common deployment error: creating well-formed fragments that Xray never loads.
Runtime identity
- Inbound tag
- socks-in
- Local port
- 10808
- Protocol
- socks
- Domain strategy
- UseIP
The application-facing listener should have one stable role and port.
Policy identity
- Proxy tag
- proxy-out
- Direct tag
- direct-out
- Block tag
- block-out
- Fallback
- proxy-out
Routing rules should refer to roles rather than changing server labels.
Keep a short design document beside the source fragments. Record the listening ports, expected traffic types, DNS policy, default outbound, and the owner of each secret. This is especially important when the same Xray core serves v2rayN on a workstation and a transparent-proxy inbound on a gateway. The transport path differs, but the naming contract and routing assumptions should remain explicit.
Split source files and assemble one runtime JSON
A practical source layout separates concerns without pretending that Xray itself supports arbitrary imports. One possible structure is shown below. The names are conventions; the important part is that every fragment has one responsibility and the assembly process is deterministic.
config-source/
├── 00-log.json
├── 10-inbounds.json
├── 20-outbounds.json
├── 30-dns.json
├── 40-routing.json
├── 50-observability.json
├── secrets/
│ ├── proxy.env
│ └── credentials.json
└── build-config.py
runtime/
└── config.json
There are two safe assembly patterns. The first is a structured generator that loads objects and arrays, validates required keys, inserts environment-specific values, and writes one complete runtime/config.json. The second is a template system that produces JSON text and then validates the output with a JSON parser. The structured approach is usually safer because it can reject duplicate tags, missing outbounds, or a routing rule that refers to an undefined tag before the file reaches Xray.
Define stable tags
Write the inbound, outbound, DNS, and routing tag names first. Treat them as an interface and do not rename them casually.
Build each module
Keep logging, inbounds, outbounds, DNS, and routing in separate source objects. Do not duplicate the same policy in several files.
Inject secrets
Load UUIDs, passwords, private keys, and subscription-specific values from protected runtime inputs rather than committing them to generic templates.
Validate references
Check JSON syntax, duplicate tags, listener collisions, outbound references, and routing order before writing the runtime file.
Test before restart
Run Xray’s configuration test against the assembled file, preserve the previous working file, and restart only after validation succeeds.
For a local SOCKS listener, the resulting module may resemble the following. The exact fields depend on whether applications send domains or already-resolved IP addresses. sniffing can help recover domain information for supported traffic, but it is not a substitute for a deliberate DNS design.
{
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"tag": "socks-in",
"settings": {
"auth": "noauth",
"udp": true
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
Do not expose a local debugging inbound on 0.0.0.0 unless the host firewall, authentication, and access scope have been designed for it. Binding to 127.0.0.1 keeps a desktop listener local. A gateway inbound has a different trust boundary and should be bound to the intended LAN address or controlled interface, not opened globally by habit.
Build DNS and routing as one policy
DNS and routing are often configured separately, but their decisions interact. If an application resolves a domain through the operating system and then supplies only an IP address to the core, domain-based rules may no longer have the original name available. If a direct DNS server is used for every query, the connection itself may be proxied while the domain lookup remains visible to the local network. A good design therefore states which resolver handles which category and whether the resolver is reached directly or through a controlled outbound.
For a desktop profile, start with a narrow policy rather than adding many public resolvers. Local names may need the LAN resolver, domestic domains may use a direct resolver, and proxy-bound domains may need remote resolution through the proxy path. The exact DNS object depends on the Xray version and deployment mode, so validate field names against the installed core documentation instead of copying options from an unrelated client format.
Recommended policy: separate destination policy from resolver policy
Destination routing
- Private ranges go direct
- Explicit blocked categories go to block
- Remaining traffic uses proxy
DNS routing
- Local names use the LAN resolver
- Direct categories use the direct path
- Proxy categories resolve remotely
The connection route and the DNS route should be reviewed together; securing only one of them leaves an avoidable leak path.
Routing rules are evaluated from top to bottom. Xray does not collect every matching rule and then select the most specific one. Put private addresses and explicit exceptions before broad geosite or geoip rules, and place the catch-all rule last. A common policy skeleton is:
{
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct-out"
},
{
"type": "field",
"domain": ["geosite:category-ads-all"],
"outboundTag": "block-out"
},
{
"type": "field",
"domain": ["geosite:cn"],
"outboundTag": "direct-out"
},
{
"type": "field",
"ip": ["geoip:cn"],
"outboundTag": "direct-out"
},
{
"type": "field",
"network": "tcp,udp",
"outboundTag": "proxy-out"
}
]
}
}
The example is intentionally conservative. geoip:private protects router interfaces, printers, storage devices, and other local destinations. The advertising category is only an example of an explicit block policy and should be removed if it does not match your requirements. The final rule catches traffic that did not match earlier exceptions. If the default should be direct instead, change the final outbound deliberately and document that decision; never leave the default to an accidental rule order.
Operational conclusion: the fallback rule defines your risk
A final proxy fallback is usually safer for privacy because unknown destinations do not silently bypass the tunnel. A final direct fallback is easier to diagnose and may reduce latency, but it can expose new domains until a specific rule is added. Choose one intentionally and test an unknown destination after every policy change.
Protect secrets and keep modules reusable
Modular configuration is valuable only when the reusable parts do not contain credentials copied across machines. A VLESS UUID, VMess identifier, Trojan password, Reality private key, API token, and subscription URL should be treated as secret material. Do not place them in a public example, a shared screenshot, a shell history entry, or a verbose debug log. A configuration backup that contains a live credential should have the same protection as the credential itself.
Use a deployment-specific secret file with restrictive permissions, an operating-system secret store, or an environment-injection mechanism provided by the service manager. Keep the generic outbound structure in source control or a private configuration repository, but inject the address, port, UUID, server name, and security values during the build step. The assembled runtime file should be readable only by the account that runs Xray. If a secret appears in a URL, remember that terminal history, process listings, and monitoring tools may record it.
Separate stable transport fields from rotating server values. For example, the reusable VLESS module can define the protocol, stream settings, network type, TLS or Reality mode, and expected flow, while a deployment-specific file supplies the endpoint and user identifier. Do not use this separation to hide incompatibilities: the server address, port, UUID, SNI, public key, short identifier, fingerprint, and flow still need to match the server-side configuration exactly.
Can I put JSON fragments in a directory and point Xray at that directory?
Not by assumption. Xray must receive a complete configuration in the schema supported by the installed core. Use a generator or deployment tool to assemble fragments, then pass the resulting file to the core.
Why does valid JSON still fail when Xray starts?
JSON syntax is only the first check. The file may contain an unsupported field, a duplicate tag, an undefined outbound reference, an invalid protocol setting, or a listener port already occupied by another process.
Should every domain be forced through one remote DNS server?
Not necessarily. Define resolver rules according to local-name access, direct destinations, and proxy destinations. Then test both the DNS query path and the connection path instead of judging the policy from browser access alone.
Is sniffing enough to prevent DNS leaks?
No. Sniffing may recover domains from supported traffic, but it does not control every application query or resolver. Combine it with an intentional DNS policy and a controlled TUN or inbound design.
Validate, debug, and roll back safely
Validation should happen in layers. First parse the assembled file with a strict JSON parser. Next run the Xray configuration test using the installed binary and the exact runtime path used by the service. Then check semantic references: every outboundTag must exist, every listener must have a unique address and port, and every rule must use fields supported by the selected core version. Finally perform a live test with one known direct destination, one known proxied destination, one private address, and one intentionally blocked destination.
Use separate logs for access events and errors when the deployment permits it. During a short diagnostic window, enable enough detail to identify the inbound tag, selected outbound, DNS failure, or transport handshake problem. Do not leave verbose logs enabled indefinitely if they expose domains, addresses, or request metadata. When a failure occurs, compare the new assembled file with the previous working file and change one module at a time.
# Example validation sequence
python3 build-config.py
python3 -m json.tool runtime/config.json > /dev/null
xray run -test -config runtime/config.json
systemctl restart xray
journalctl -u xray --since "2 minutes ago"
The command names can differ by operating system and package layout, so use the Xray executable and service definition actually installed on the host. A successful syntax test does not prove that the remote server is reachable. If the core starts but connections fail, inspect the selected outbound, remote address resolution, TCP reachability, TLS or Reality parameters, and server-side logs in that order.
Common symptoms have different boundaries. “Address already in use” points to a local listener collision, often caused by another Xray instance or a client already occupying port 10808. “Unknown outbound tag” indicates a build or naming error between routing and outbounds. A TLS handshake failure requires checking time, SNI, certificate expectations, fingerprint, and transport settings rather than changing routing rules. A connection that succeeds while DNS queries appear on the LAN indicates a resolver-policy problem, not necessarily a broken proxy outbound.
Safe rollback rule: keep the last known-good runtime file, validate the new file before replacing it, and make the service restart point to a stable path. If the new configuration fails, restore the previous file rather than manually editing several modules while the service is down.
Finally, test the configuration after core upgrades, subscription changes, and platform network changes. Xray JSON fields, geo data behavior, client-generated settings, and service wrappers can evolve independently. A modular source tree makes review easier, but it does not remove the need for a complete runtime test. Treat the generated JSON as a deployable artifact, keep its provenance clear, and record which core version, rule data, and secret set produced it.