Open-source project
MHSanaei/3x-ui avatar
MHSanaei/3x-ui

3x-ui: Xray inbounds with per-client quotas and a personal-use warning

GitHub describes it as Xray panel supporting multi-protocol multi-user expire day & traffic & IP limit (Vmess, Vless, Trojan, ShadowSocks, Wireguard, Hysteria, Tunnel, Mixed, HTTP, Tun, MTProto). The repository metadata lists Go as its primary language. The metadata lists the GPL-3.0 license. This article stays within the project description and details documented in the GitHub repository README.

47,109 stars11,893 forksGoGPL-3.0

At a glance

What is it?
3x-ui is a Go web panel that manages Xray-core servers, adding per-client traffic accounting, expiry dates, IP limits and a subscription server on top of the original X-UI. The install is one piped shell command that invents your credentials for you, and the project states it is not for production use.
Who is it for?
3x-ui fits one operator on a VPS who wants per-client quotas, expiry dates, IP limits and a subscription link without hand-writing Xray JSON. It does not fit a production service, because the project itself states the software is for personal use only and not a production environment.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The install command invents your username, password and access path

There is one documented install path, and it pipes a remote script straight into bash:

bash
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)

The same script takes a tag, so a specific version such as v3.7.0 is an argument rather than a separate command, and passing dev-latest gets the rolling per-commit pre-release from main instead of a stable release. What you get back is not a login you chose. During installation a random username, password and access path are generated, and `x-ui` then opens a menu for starting and stopping the service, viewing or resetting those credentials, and managing SSL certificates.

Two details matter for automation. Every release asset is published with a .sha256 sum beside it, and both install.sh and the updater verify the archive against that sum and abort on a mismatch, so a corrupted or substituted download fails loudly instead of installing. For unattended runs, set XUI_NONINTERACTIVE=1, or pipe with no TTY, and the installer completes with zero prompts and writes the generated credentials to /etc/x-ui/install-result.env. The deploy/ directory holds cloud-init user-data for Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure and Oracle.

Three service units and three config paths for one binary

A 3x-ui host runs one Go binary, but the repository ships the service definitions per init system: x-ui.service.debian, x-ui.service.arch and x-ui.service.rhel, plus an x-ui.rc for the remaining systems. The configuration follows the same split. There is no YAML or JSON settings file for the panel; runtime configuration comes from XUI_* environment variables, and the installer writes them to the service environment file for your distribution, which is /etc/default/x-ui on Debian-family systems, /etc/conf.d/x-ui on Arch-family systems, or /etc/sysconfig/x-ui on RHEL-family systems.

The failure mode that follows is quiet. Put XUI_DB_TYPE in the Debian path on an Arch box and the unit never reads it, so the panel starts on defaults and the change appears to have been ignored. .env.example exists mainly to spell this out: the active block at the top is a developer bootstrap that lets `cp .env.example .env` work for an unprivileged `go run .` without root, and everything below it is a commented reference of the available options. It also notes that the same values go in docker-compose.yml or in a `docker run -e` list under Docker, and that a change to any of them needs a `systemctl restart x-ui` to take effect.

SQLite is the default and migrate-db leaves the old file where it is

The backend is chosen at install time and can be changed at runtime through two variables:

code
XUI_DB_TYPE=postgres
XUI_DB_DSN=postgres://xui:[email protected]:5432/xui?sslmode=disable

SQLite is the default, a single file at /etc/x-ui/x-ui.db, described as zero setup and aimed at small and medium deployments. PostgreSQL is the recommendation once client counts get high or you run more than one node, and the installer can either install it locally or accept a DSN pointing at a server you already have. Moving an existing install across is a CLI subcommand rather than a settings change:

bash
x-ui migrate-db --dsn "postgres://xui:[email protected]:5432/xui?sslmode=disable"
# then set XUI_DB_TYPE and XUI_DB_DSN in /etc/default/x-ui and restart:
systemctl restart x-ui

The instruction after the command is the one people skip. The source SQLite file is deliberately left untouched, with removal left as a manual step once you have verified the new backend. Until you do that verification you have two divergent copies of your client list, and which one the panel reads depends entirely on the two variables and the restart.

Without NET_ADMIN the Fail2ban ban is logged and never applied

Per-client IP limits are enforced by a bundled Fail2ban, enabled in the compose file with XUI_ENABLE_FAIL2BAN set to true. The enforcement path is iptables, and iptables in a container needs capabilities. The compose file therefore asks for two:

yaml
    cap_add:
      - NET_ADMIN
      - NET_RAW

NET_RAW is there to cover ip6tables, so both address families can be filtered. The consequence of dropping them is not an error. The compose file spells it out: without those caps a ban is logged and shown in fail2ban status but never actually applied. You get a jail that looks healthy, an IP that keeps connecting, and a per-client limit that is decorative.

That makes this the first thing to check after a container install, because the panel's own feature list promises IP limits with trusted-address exemptions and HWID device caps, and none of that means anything while the ban path is inert. The same file also warns that a memory cap interacts with the Go heap: when mem_limit is set, the panel derives a soft limit around 90 percent of it so the garbage collector runs before the OOM killer, while the commented defaults instead tune GOGC and a periodic memory release interval.

AmneziaWG lives inside the panel process on a gVisor netstack

WireGuard support comes in two forms, and the difference is architectural. AmneziaWG is described as built in, running inside the panel on a userspace network stack with no kernel module, no DKMS and no extra packages to install. In the compose file the same point is made from the other direction: AmneziaWG runs embedded in the panel process, as amneziawg-go over a gVisor userspace netstack, and you publish its UDP listen port to use it.

Both halves of that sentence have consequences. The convenience is real, since there is no host package to build and no module to survive a kernel upgrade, and the Go module confirms the stack with github.com/amnezia-vpn/amneziawg-go/v3 alongside a gvisor dependency. The cost is that the data path lives in the same process as the web panel, and its UDP port has to be published explicitly. A conventional WireGuard inbound in the same panel is configured in Xray instead, so the two coexist with different resource and port behaviour.

TUIC v5 arrives as a sidecar rather than embedded, and it is the one entry in the list that does its own traffic metering: native UDP relay metering, 0-RTT handshakes and BBR congestion control. MTProto is applied live, with per-client FakeTLS secrets, ad-tags and quotas, without dropping existing connections.

CGO is mandatory, so the Go build needs a C toolchain

The go.mod declares go 1.27.1 and carries mattn/go-sqlite3, which is a cgo binding. That single dependency shapes the build. The Dockerfile builder stage installs build-base and gcc, sets two environment variables and then compiles:

dockerfile
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -ldflags "-w -s" -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"

CGO_ENABLED=1 is not a default you can drop, and CGO_CFLAGS is what makes the large-file support resolve on 32-bit targets, which matters given the architecture list runs from amd64 and 386 down through armv5, armv6, armv7 and arm64 to s390x. DockerInit.sh then takes TARGETARCH and produces the per-architecture asset, which is the same set of files the sha256 sums cover on the install path.

The rest of the image is a three-stage build: node:22-alpine runs npm ci and npm run build for the frontend/ directory, the Go stage produces the binary, and the final alpine layer copies the built tree plus DockerEntrypoint.sh and x-ui.sh, installs fail2ban, openssl and bash, and rewrites jail.conf into jail.local with the ssh and sshd jails disabled. Timezone is set to Asia/Tehran in the final stage, which is a detail worth knowing if you are reading logs.

The project says personal use only, and ships management bots on the same install

One block in the README settles the scope question before anything else: this project is intended for personal use only, and should not be used for illegal purposes or in a production environment. That is the project's own statement, not an inference, and it is worth taking at face value when you weigh a control panel that holds every client's traffic quota, expiry date and IP limit for your server.

The surface area is wide for something described as personal. Alongside the panel there are Telegram and Discord bots for remote monitoring and management, a RESTful API with scoped and optionally expiring tokens plus an in-panel API reference, multi-node support that can clone an inbound onto other servers, and a built-in subscription server that serves raw, JSON and Clash output chosen from the client's User-Agent. Anyone automating this install is putting three management channels behind one set of randomly generated credentials.

As a fork, its lineage is explicit: it is built on the original X-UI project and adds broader protocol coverage, stability work and per-client traffic accounting. No competing panel is named for comparison, so a team choosing between panels has to do that research outside the project. On the storage side, SQLite and PostgreSQL are the only two backends named, and the migration path between them is a single subcommand.

Editorial conclusion

3x-ui fits one operator on a VPS who wants per-client quotas, expiry dates, IP limits and a subscription link without hand-writing Xray JSON. It does not fit a production service, because the project itself states the software is for personal use only and not a production environment. Verify first that the running container actually holds NET_ADMIN, because without it the Fail2ban integration records an IP ban and never applies it.

Frequently asked questions

What is 3X-UI used for?

It is a web control panel for managing Xray-core servers, covering inbound protocols including VLESS, VMess, Trojan, Shadowsocks, WireGuard, AmneziaWG, TUIC v5, Hysteria2, MTProto, HTTP, SOCKS, Dokodemo-door and TUN. On top of that it adds per-client traffic quotas, expiry dates, IP limits, a subscription server and multi-node management.

How do I install 3X-UI?

Run the installer from the quick start, which generates a random username, password and access path during installation, then use the x-ui command for the management menu. Passing dev-latest instead of a tag installs the rolling per-commit pre-release from main, and setting XUI_NONINTERACTIVE=1 runs it with no prompts and writes the credentials to /etc/x-ui/install-result.env.

Where can I find the 3X-UI configuration file?

There is no single config file. The panel reads XUI_* environment variables, and on a script install the installer writes them to the distribution's service environment file: /etc/default/x-ui, /etc/conf.d/x-ui or /etc/sysconfig/x-ui. Under Docker the same variables go into docker-compose.yml or a docker run -e list, and any change needs systemctl restart x-ui.

Is 3xui free to use?

The project is licensed GPL-3.0 and is an enhanced fork of the original X-UI project. It also states that it is intended for personal use only and should not be used in a production environment, so the licence does not come with any support commitment for a service you depend on.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mhsanaei-3x-ui.svg)](https://hysenlabs.com/projects/mhsanaei-3x-ui)