diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..99bb78cc
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+.git
+db
+cert
+*.log
+Dockerfile
+docker-compose.yml
+.tmp
+.idea
+.vscode
+LICENSE
+README.*
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 8fa4eeb0..69b9c69d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# shared volume
+geodata/
+
# Ignore editor and IDE settings
.idea/
.vscode/
diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh
index 7511d2ea..455c3211 100644
--- a/DockerEntrypoint.sh
+++ b/DockerEntrypoint.sh
@@ -1,7 +1,28 @@
#!/bin/sh
+set -eu
+
+: "${MAX_GEODATA_DIR_WAIT:=30}"
+: "${WAIT_INTERVAL:=10}"
+: "${GEODATA_DIR:?GEODATA_DIR is required}"
+
+FINISH_FILE="$GEODATA_DIR/cron-job-finished.txt"
+ELAPSED=0
+
+while [ ! -f "$FINISH_FILE" ] && [ "$ELAPSED" -lt "$MAX_GEODATA_DIR_WAIT" ]; do
+ echo "Waiting for geodata initialization... ($ELAPSED/$MAX_GEODATA_DIR_WAIT seconds)"
+ sleep $WAIT_INTERVAL
+ ELAPSED=$((ELAPSED + WAIT_INTERVAL))
+done
+
+if [ ! -f "$FINISH_FILE" ]; then
+ echo "ERROR: Geodata initialization timed out after $MAX_GEODATA_DIR_WAIT seconds"
+ echo "Container startup aborted."
+ exit 1
+fi
+
# Start fail2ban
-[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
+[ "$XUI_ENABLE_FAIL2BAN" = "true" ] && fail2ban-client -x start
# Run x-ui
exec /app/x-ui
diff --git a/DockerInit.sh b/DockerInit.sh
deleted file mode 100755
index debfbbb8..00000000
--- a/DockerInit.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/sh
-case $1 in
- amd64)
- ARCH="64"
- FNAME="amd64"
- ;;
- i386)
- ARCH="32"
- FNAME="i386"
- ;;
- armv8 | arm64 | aarch64)
- ARCH="arm64-v8a"
- FNAME="arm64"
- ;;
- armv7 | arm | arm32)
- ARCH="arm32-v7a"
- FNAME="arm32"
- ;;
- armv6)
- ARCH="arm32-v6"
- FNAME="armv6"
- ;;
- *)
- ARCH="64"
- FNAME="amd64"
- ;;
-esac
-mkdir -p build/bin
-cd build/bin
-curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip"
-unzip "Xray-linux-${ARCH}.zip"
-rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
-mv xray "xray-linux-${FNAME}"
-curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
-curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
-curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
-curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
-curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
-curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
-cd ../../
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index ec7a6e7b..ab861c8c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,40 +2,40 @@
# Stage: Builder
# ========================================================
FROM golang:1.25-alpine AS builder
-WORKDIR /app
-ARG TARGETARCH
-RUN apk --no-cache --update add \
+WORKDIR /app
+
+RUN apk add --no-cache \
build-base \
- gcc \
- curl \
- unzip
+ gcc
+
+# docker CACHE
+COPY go.mod go.sum ./
+RUN go mod download
COPY . .
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"
# ========================================================
# Stage: Final Image of 3x-ui
# ========================================================
FROM alpine
-ENV TZ=Asia/Tehran
+
WORKDIR /app
-RUN apk add --no-cache --update \
+RUN apk add --no-cache \
ca-certificates \
tzdata \
fail2ban \
bash
+COPY DockerEntrypoint.sh /app/
COPY --from=builder /app/build/ /app/
-COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
-
# Configure fail2ban
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
@@ -51,5 +51,5 @@ RUN chmod +x \
ENV XUI_ENABLE_FAIL2BAN="true"
EXPOSE 2053
VOLUME [ "/etc/x-ui" ]
-CMD [ "./x-ui" ]
+
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
diff --git a/README.md b/README.md
index f00a2fb0..1fd770ce 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](/README.md) | [فارسی](docs/README.fa_IR.md) | [العربية](docs/README.ar_EG.md) | [中文](docs/README.zh_CN.md) | [Español](docs/README.es_ES.md) | [Русский](docs/README.ru_RU.md)
-
-
+
+
@@ -44,12 +44,12 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
**If this project is helpful to you, you may wish to give it a**:star2:
-
+
-
+
-
+
## Stargazers over Time
diff --git a/docker-compose.yml b/docker-compose.yml
index 198df198..0fa748c1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,16 +1,57 @@
services:
- 3xui:
+ 3x-ui:
build:
context: .
- dockerfile: ./Dockerfile
container_name: 3xui_app
- # hostname: yourhostname <- optional
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
+ - $PWD/geodata/:/app/bin
environment:
+ TZ: "Asia/Tehran"
XRAY_VMESS_AEAD_FORCED: "false"
XUI_ENABLE_FAIL2BAN: "true"
+ GEODATA_DIR: "/app/bin"
tty: true
network_mode: host
restart: unless-stopped
+ depends_on:
+ - geodata-cron
+
+ docker-proxy:
+ image: tecnativa/docker-socket-proxy
+ container_name: docker_proxy
+ restart: unless-stopped
+ environment:
+ - CONTAINERS=1
+ - POST=1
+ - ALLOW_RESTARTS=1
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ networks:
+ - docker-internal
+
+ geodata-cron:
+ build:
+ context: docker-cron-runner
+ args:
+ XRAY_VERSION: "${XRAY_VERSION:-v25.10.15}"
+ XRAY_BUILD_DIR: "/app/xray"
+ container_name: geodata_cron
+ restart: unless-stopped
+ depends_on:
+ - docker-proxy
+ environment:
+ TZ: "UTC"
+ DOCKER_PROXY_URL: "http://docker-proxy:2375"
+ TARGET_CONTAINER_NAME: "3xui_app"
+ CRON_SCHEDULE: "0 */6 * * *"
+ SHARED_VOLUME_PATH: "/app/bin"
+ volumes:
+ - $PWD/geodata/:/app/bin/
+ networks:
+ - docker-internal
+
+networks:
+ docker-internal:
+ driver: bridge
\ No newline at end of file
diff --git a/docker-cron-runner/Dockerfile b/docker-cron-runner/Dockerfile
new file mode 100644
index 00000000..224dca41
--- /dev/null
+++ b/docker-cron-runner/Dockerfile
@@ -0,0 +1,28 @@
+FROM alpine:3.20
+
+ARG TARGETARCH
+ARG XRAY_VERSION
+ARG XRAY_BUILD_DIR
+
+WORKDIR /app
+
+RUN apk add --no-cache \
+ wget \
+ unzip \
+ curl \
+ bash \
+ ca-certificates \
+ tzdata
+
+
+COPY xray-tools.sh entrypoint.sh cron-job-script.sh ./
+
+RUN chmod +x /app/xray-tools.sh /app/entrypoint.sh /app/cron-job-script.sh \
+ && mkdir -p "$XRAY_BUILD_DIR" \
+ && ./xray-tools.sh install_xray_core "$TARGETARCH" "$XRAY_BUILD_DIR" "$XRAY_VERSION" \
+ && ./xray-tools.sh update_geodata_in_docker "$XRAY_BUILD_DIR"
+
+ENV XRAY_BUILD_DIR=${XRAY_BUILD_DIR}
+
+#CMD ["/app/entrypoint.sh"] \
+ENTRYPOINT ["/app/entrypoint.sh"]
\ No newline at end of file
diff --git a/docker-cron-runner/cron-job-script.sh b/docker-cron-runner/cron-job-script.sh
new file mode 100644
index 00000000..ec2eff8b
--- /dev/null
+++ b/docker-cron-runner/cron-job-script.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env sh
+set -eu
+
+echo "[$(date)] Starting geodata update..."
+
+FINISHED_FLAG="${SHARED_VOLUME_PATH}/cron-job-finished.txt"
+
+if [ -f "$FINISHED_FLAG" ]; then
+ rm -f "$FINISHED_FLAG"
+fi
+
+/app/xray-tools.sh update_geodata_in_docker "${SHARED_VOLUME_PATH}"
+touch "$FINISHED_FLAG"
+
+echo "[$(date)] Geodata update finished, restarting container..."
+
+HTTP_CODE=$(
+ curl -s -X POST \
+ "${DOCKER_PROXY_URL}/containers/${TARGET_CONTAINER_NAME}/restart" \
+ -o /dev/null -w "%{http_code}"
+)
+
+echo "[$(date)] Restart request sent, HTTP status: ${HTTP_CODE}"
\ No newline at end of file
diff --git a/docker-cron-runner/entrypoint.sh b/docker-cron-runner/entrypoint.sh
new file mode 100644
index 00000000..9d4cc001
--- /dev/null
+++ b/docker-cron-runner/entrypoint.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env sh
+
+set -eu
+
+: "${CRON_SCHEDULE:=0 */6 * * *}"
+: "${DOCKER_PROXY_URL:?DOCKER_PROXY_URL is required}"
+: "${TARGET_CONTAINER_NAME:?TARGET_CONTAINER_NAME is required}" # required for cron-job-script.sh for container restart
+: "${SHARED_VOLUME_PATH:?SHARED_VOLUME_PATH is required}"
+
+CRON_ENV_FILE="/env.sh"
+
+env | grep -v '^CRON_SCHEDULE=' | sed 's/^/export /' > "$CRON_ENV_FILE"
+echo "${CRON_SCHEDULE} . ${CRON_ENV_FILE} && /app/cron-job-script.sh >> /var/log/cron.log 2>&1" > /etc/crontabs/root
+
+echo "Starting crond with schedule: ${CRON_SCHEDULE}"
+
+mkdir -p /var/log
+touch /var/log/cron.log
+
+mkdir -p "$SHARED_VOLUME_PATH"
+cp -r "$XRAY_BUILD_DIR"/* "$SHARED_VOLUME_PATH"/
+
+touch "$SHARED_VOLUME_PATH/cron-job-finished.txt" # cron job execution imitation
+
+exec crond -f -l 2
\ No newline at end of file
diff --git a/docker-cron-runner/xray-tools.sh b/docker-cron-runner/xray-tools.sh
new file mode 100644
index 00000000..35ec12b6
--- /dev/null
+++ b/docker-cron-runner/xray-tools.sh
@@ -0,0 +1,173 @@
+#!/bin/sh
+
+safe_download_and_update() {
+ url="$1"
+ dest="$2"
+
+ # Create a temporary file
+ tmp=$(mktemp "${dest}.XXXXXX") || return 1
+
+ # Download file into a temporary location
+ if wget -q -O "$tmp" "$url"; then
+ # Check that the downloaded file is not empty
+ if [ -s "$tmp" ]; then
+ # Atomically replace the destination file
+ mv "$tmp" "$dest"
+ echo "[OK] Downloaded: $dest"
+ else
+ echo "[ERR] Downloaded file is empty: $url"
+ rm -f "$tmp"
+ return 1
+ fi
+ else
+ echo "[ERR] Failed to download: $url"
+ rm -f "$tmp"
+ return 1
+ fi
+}
+
+update_all_geofiles() {
+ update_main_geofiles
+ update_ir_geofiles
+ update_ru_geofiles
+}
+
+update_main_geofiles() {
+ safe_download_and_update \
+ "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" \
+ "geoip.dat"
+
+ safe_download_and_update \
+ "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" \
+ "geosite.dat"
+}
+
+update_ir_geofiles() {
+ safe_download_and_update \
+ "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" \
+ "geoip_IR.dat"
+
+ safe_download_and_update \
+ "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" \
+ "geosite_IR.dat"
+}
+
+update_ru_geofiles() {
+ safe_download_and_update \
+ "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" \
+ "geoip_RU.dat"
+
+ safe_download_and_update \
+ "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" \
+ "geosite_RU.dat"
+}
+
+update_geodata_in_docker() {
+ XRAYDIR="$1"
+ OLD_DIR=$(pwd)
+ trap 'cd "$OLD_DIR"' EXIT
+
+ echo "[$(date)] Running update_geodata"
+
+ if [ ! -d "$XRAYDIR" ]; then
+ mkdir -p "$XRAYDIR"
+ fi
+ cd "$XRAYDIR"
+
+ update_all_geofiles
+ echo "[$(date)] All geo files have been updated successfully!"
+}
+
+
+install_xray_core() {
+ TARGETARCH="$1"
+ XRAYDIR="$2"
+ XRAY_VERSION="$3"
+
+ OLD_DIR=$(pwd)
+ trap 'cd "$OLD_DIR"' EXIT
+
+ echo "[$(date)] Running install_xray_core"
+
+ case $1 in
+ amd64)
+ ARCH="64"
+ FNAME="amd64"
+ ;;
+ i386)
+ ARCH="32"
+ FNAME="i386"
+ ;;
+ armv8 | arm64 | aarch64)
+ ARCH="arm64-v8a"
+ FNAME="arm64"
+ ;;
+ armv7 | arm | arm32)
+ ARCH="arm32-v7a"
+ FNAME="arm32"
+ ;;
+ armv6)
+ ARCH="arm32-v6"
+ FNAME="armv6"
+ ;;
+ *)
+ ARCH="64"
+ FNAME="amd64"
+ ;;
+ esac
+
+ if [ ! -d "$XRAYDIR" ]; then
+ mkdir -p "$XRAYDIR"
+ fi
+ cd "$XRAYDIR"
+
+ wget -q "https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/Xray-linux-${ARCH}.zip"
+
+ # Validate the downloaded zip file
+ if [ ! -f "Xray-linux-${ARCH}.zip" ] || [ ! -s "Xray-linux-${ARCH}.zip" ]; then
+ echo "[ERR] Failed to download Xray-core zip or file is empty"
+ exit 1
+ fi
+
+ unzip -q "Xray-linux-${ARCH}.zip" -d ./xray-unzip
+
+ # Validate the extracted xray binary
+ if [ -f "./xray-unzip/xray" ]; then
+ cp ./xray-unzip/xray ./"xray-linux-${FNAME}"
+ rm -r xray-unzip
+ rm "Xray-linux-${ARCH}.zip"
+ else
+ echo "[ERR] Failed to extract xray binary"
+ exit 1
+ fi
+}
+
+if [ "${0##*/}" = "xray-tools.sh" ]; then
+ cmd="$1"
+ shift || true
+
+ case "$cmd" in
+ install_xray_core)
+ # args: TARGETARCH XRAYDIR XRAY_VERSION
+ install_xray_core "$@"
+ ;;
+ update_geodata_in_docker)
+ # args: XRAYDIR
+ update_geodata_in_docker "$@"
+ ;;
+ update_all_geofiles)
+ update_all_geofiles
+ ;;
+ ""|help|-h|--help)
+ echo "Usage:"
+ echo " $0 install_xray_core TARGETARCH XRAYDIR XRAY_VERSION"
+ echo " $0 update_geodata_in_docker XRAYDIR"
+ exit 0
+ ;;
+ *)
+ echo "Unknown command: $cmd" >&2
+ echo "Try: $0 help" >&2
+ exit 1
+ ;;
+ esac
+fi
\ No newline at end of file
diff --git a/README.ar_EG.md b/docs/README.ar_EG.md
similarity index 94%
rename from README.ar_EG.md
rename to docs/README.ar_EG.md
index 01acad34..04774f33 100644
--- a/README.ar_EG.md
+++ b/docs/README.ar_EG.md
@@ -1,4 +1,4 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)
@@ -46,6 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
+
diff --git a/README.es_ES.md b/docs/README.es_ES.md
similarity index 94%
rename from README.es_ES.md
rename to docs/README.es_ES.md
index 63d6ce49..915ab48d 100644
--- a/README.es_ES.md
+++ b/docs/README.es_ES.md
@@ -1,4 +1,4 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)
@@ -46,7 +46,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
-
+
diff --git a/README.fa_IR.md b/docs/README.fa_IR.md
similarity index 94%
rename from README.fa_IR.md
rename to docs/README.fa_IR.md
index 94165260..15223eef 100644
--- a/README.fa_IR.md
+++ b/docs/README.fa_IR.md
@@ -1,4 +1,4 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)
@@ -46,7 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
-
+
diff --git a/README.ru_RU.md b/docs/README.ru_RU.md
similarity index 95%
rename from README.ru_RU.md
rename to docs/README.ru_RU.md
index 6623a801..5ee8ccd1 100644
--- a/README.ru_RU.md
+++ b/docs/README.ru_RU.md
@@ -1,4 +1,4 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)
@@ -46,7 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
-
+
diff --git a/README.zh_CN.md b/docs/README.zh_CN.md
similarity index 93%
rename from README.zh_CN.md
rename to docs/README.zh_CN.md
index 6eb30ee0..e32f47ac 100644
--- a/README.zh_CN.md
+++ b/docs/README.zh_CN.md
@@ -1,4 +1,5 @@
-[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
+[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)
+
@@ -46,7 +47,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
-
+
diff --git a/docs/media/3x-ui-dark.png b/docs/media/3x-ui-dark.png
new file mode 100644
index 00000000..e5f76b19
Binary files /dev/null and b/docs/media/3x-ui-dark.png differ
diff --git a/docs/media/3x-ui-light.png b/docs/media/3x-ui-light.png
new file mode 100644
index 00000000..a77c830d
Binary files /dev/null and b/docs/media/3x-ui-light.png differ
diff --git a/docs/media/default-yellow.png b/docs/media/default-yellow.png
new file mode 100644
index 00000000..21ed36b7
Binary files /dev/null and b/docs/media/default-yellow.png differ
diff --git a/docs/media/donation-button-black.svg b/docs/media/donation-button-black.svg
new file mode 100644
index 00000000..25217129
--- /dev/null
+++ b/docs/media/donation-button-black.svg
@@ -0,0 +1 @@
+
\ No newline at end of file