Jelajahi Sumber

chore(deploy): drop the AWS golden-image build stack

Remove the release-driven Packer AMI/qcow2 pipeline and everything that existed only to feed it: the image.yml workflow, deploy/packer, deploy/lightsail, deploy/firstboot, the AWS Marketplace checklist, and the first-boot smoke test/job.

Keep the cloud-agnostic unattended-install path (cloud-init + install.sh non-interactive) and the Hetzner notes, which never depended on the workflow. Hetzner's snapshot path is dropped too since it relied on firstboot to avoid admin/admin on clones; cloud-init regenerates per-instance credentials on its own.

Update deploy/README, the cloud-init and Hetzner docs, the root README plus its six translations, and .gitattributes to match.
MHSanaei 1 hari lalu
induk
melakukan
30796dc2ce

+ 1 - 4
.gitattributes

@@ -5,8 +5,5 @@ frontend/src/generated/** text eol=lf
 frontend/public/openapi.json text eol=lf
 frontend/public/openapi.json text eol=lf
 frontend/src/test/__snapshots__/** text eol=lf
 frontend/src/test/__snapshots__/** text eol=lf
 
 
-# Cloud-image deploy assets are consumed on Linux — force LF regardless of host.
-*.service text eol=lf
-deploy/**/*.service text eol=lf
-deploy/**/*.hcl text eol=lf
+# Cloud-init deploy assets are consumed on Linux — force LF regardless of host.
 deploy/**/*.yaml text eol=lf
 deploy/**/*.yaml text eol=lf

+ 0 - 260
.github/workflows/image.yml

@@ -1,260 +0,0 @@
-name: Build Cloud Images
-
-# Build golden cloud images from a published release, for amd64 and arm64:
-#   * qemu       -> qcow2 attached to the GitHub release (always)
-#   * amazon-ebs -> AWS AMI (only when AWS credentials are configured)
-#
-# Images contain NO database and NO baked credentials; first boot generates
-# unique per-instance credentials (see deploy/firstboot + deploy/packer).
-
-on:
-  release:
-    types: [published]
-  workflow_dispatch:
-    inputs:
-      tag:
-        description: "Release tag to build images for (e.g. v3.3.1)"
-        required: true
-        type: string
-
-permissions:
-  contents: write
-
-concurrency:
-  group: image-${{ github.event.release.tag_name || inputs.tag }}
-  cancel-in-progress: false
-
-jobs:
-  # Resolve the tag and wait until BOTH arch tarballs are actually published
-  # (the release matrix uploads assets one by one, so 'published' can fire
-  # before the tarballs exist).
-  setup:
-    runs-on: ubuntu-latest
-    outputs:
-      tag: ${{ steps.resolve.outputs.tag }}
-    steps:
-      - name: Resolve tag
-        id: resolve
-        run: |
-          if [ "${{ github.event_name }}" = "release" ]; then
-            TAG="${{ github.event.release.tag_name }}"
-          else
-            TAG="${{ inputs.tag }}"
-          fi
-          [ -n "$TAG" ] || { echo "::error::no tag resolved"; exit 1; }
-          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
-
-      - name: Wait for released binary assets (amd64 + arm64)
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          TAG: ${{ steps.resolve.outputs.tag }}
-        run: |
-          want="x-ui-linux-amd64.tar.gz x-ui-linux-arm64.tar.gz"
-          for i in $(seq 1 30); do
-            names=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name')
-            missing=""
-            for w in $want; do
-              echo "$names" | grep -qx "$w" || missing="$missing $w"
-            done
-            if [ -z "$missing" ]; then
-              echo "All assets present on $TAG"
-              exit 0
-            fi
-            echo "Waiting for$missing on $TAG ($i/30)..."
-            sleep 20
-          done
-          echo "::error::missing release assets on $TAG after 10 minutes:$missing"
-          exit 1
-
-  # Gate the AWS AMI build so forks without secrets skip it cleanly
-  # (secrets cannot be referenced directly in job-level `if`).
-  check-aws:
-    runs-on: ubuntu-latest
-    outputs:
-      enabled: ${{ steps.c.outputs.enabled }}
-      use_oidc: ${{ steps.c.outputs.use_oidc }}
-    steps:
-      - id: c
-        env:
-          ROLE: ${{ secrets.AWS_ROLE_ARN }}
-          KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
-        run: |
-          if [ -n "$ROLE" ]; then
-            echo "enabled=true" >> "$GITHUB_OUTPUT"
-            echo "use_oidc=true" >> "$GITHUB_OUTPUT"
-          elif [ -n "$KEY" ]; then
-            echo "enabled=true" >> "$GITHUB_OUTPUT"
-            echo "use_oidc=false" >> "$GITHUB_OUTPUT"
-          else
-            echo "enabled=false" >> "$GITHUB_OUTPUT"
-            echo "use_oidc=false" >> "$GITHUB_OUTPUT"
-            echo "::notice::No AWS credentials configured; skipping the AMI build."
-          fi
-
-  qemu-image:
-    needs: setup
-    timeout-minutes: 90
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - arch: amd64
-            runner: ubuntu-latest
-            qemu_pkgs: qemu-system-x86 qemu-utils
-          - arch: arm64
-            runner: ubuntu-24.04-arm
-            qemu_pkgs: qemu-system-arm qemu-efi-aarch64 qemu-utils
-    runs-on: ${{ matrix.runner }}
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v7
-
-      - name: Install QEMU
-        run: |
-          sudo apt-get update
-          sudo apt-get install -y --no-install-recommends ${{ matrix.qemu_pkgs }}
-
-      - name: Setup Packer
-        uses: hashicorp/setup-packer@v3
-        with:
-          version: latest
-
-      - name: Verify released binary asset
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          TAG: ${{ needs.setup.outputs.tag }}
-        run: |
-          mkdir -p _asset
-          gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
-            --pattern "x-ui-linux-${{ matrix.arch }}.tar.gz" --dir _asset
-          ls -la _asset
-
-      - name: Select accelerator
-        id: accel
-        run: |
-          if [ -e /dev/kvm ]; then echo "value=kvm" >> "$GITHUB_OUTPUT"; else echo "value=tcg" >> "$GITHUB_OUTPUT"; fi
-
-      - name: Packer init
-        run: packer init deploy/packer/
-
-      - name: Build qcow2 image
-        env:
-          TAG: ${{ needs.setup.outputs.tag }}
-          ACCEL: ${{ steps.accel.outputs.value }}
-        run: |
-          packer build -only='qemu.x-ui' \
-            -var "xui_version=${TAG}" \
-            -var "xui_arch=${{ matrix.arch }}" \
-            -var "qemu_accelerator=${ACCEL}" \
-            deploy/packer/
-
-      - name: Compress qcow2
-        id: pack
-        env:
-          TAG: ${{ needs.setup.outputs.tag }}
-        run: |
-          cd deploy/packer/output-qemu
-          src="3x-ui-ubuntu-24.04-${{ matrix.arch }}.qcow2"
-          out="3x-ui-ubuntu-24.04-${TAG}-${{ matrix.arch }}.qcow2.xz"
-          xz -T0 -6 -c "$src" > "$out"
-          sha256sum "$out" > "${out}.sha256"
-          echo "file=deploy/packer/output-qemu/${out}" >> "$GITHUB_OUTPUT"
-          echo "sha=deploy/packer/output-qemu/${out}.sha256" >> "$GITHUB_OUTPUT"
-          ls -la
-
-      - name: Attach qcow2 to release
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          TAG: ${{ needs.setup.outputs.tag }}
-        run: |
-          gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \
-            "${{ steps.pack.outputs.file }}" "${{ steps.pack.outputs.sha }}"
-
-      - name: Summary
-        env:
-          TAG: ${{ needs.setup.outputs.tag }}
-          ACCEL: ${{ steps.accel.outputs.value }}
-        run: |
-          {
-            echo "## QEMU image (${{ matrix.arch }})"
-            echo "- Tag: \`${TAG}\`"
-            echo "- Accelerator: \`${ACCEL}\`"
-            echo "- Attached: \`$(basename "${{ steps.pack.outputs.file }}")\`"
-          } >> "$GITHUB_STEP_SUMMARY"
-
-  ami-image:
-    needs: [setup, check-aws]
-    if: needs.check-aws.outputs.enabled == 'true'
-    runs-on: ubuntu-latest
-    timeout-minutes: 60
-    permissions:
-      contents: read
-      id-token: write
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - arch: amd64
-            instance_type: t3.small
-          - arch: arm64
-            instance_type: t4g.small
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v7
-
-      - name: Setup Packer
-        uses: hashicorp/setup-packer@v3
-        with:
-          version: latest
-
-      - name: Configure AWS credentials (OIDC)
-        if: needs.check-aws.outputs.use_oidc == 'true'
-        uses: aws-actions/configure-aws-credentials@v6
-        with:
-          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
-          aws-region: ${{ vars.AWS_REGION || 'eu-central-1' }}
-
-      - name: Configure AWS credentials (access keys)
-        if: needs.check-aws.outputs.use_oidc != 'true'
-        uses: aws-actions/configure-aws-credentials@v6
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ vars.AWS_REGION || 'eu-central-1' }}
-
-      - name: Verify released binary asset
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          TAG: ${{ needs.setup.outputs.tag }}
-        run: |
-          mkdir -p _asset
-          gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
-            --pattern "x-ui-linux-${{ matrix.arch }}.tar.gz" --dir _asset
-          ls -la _asset
-
-      - name: Packer init
-        run: packer init deploy/packer/
-
-      - name: Build AMI
-        env:
-          TAG: ${{ needs.setup.outputs.tag }}
-          REGION: ${{ vars.AWS_REGION || 'eu-central-1' }}
-        run: |
-          packer build -only='amazon-ebs.x-ui' \
-            -var "xui_version=${TAG}" \
-            -var "xui_arch=${{ matrix.arch }}" \
-            -var "instance_type=${{ matrix.instance_type }}" \
-            -var "region=${REGION}" \
-            deploy/packer/
-
-      - name: Publish AMI id to summary
-        env:
-          REGION: ${{ vars.AWS_REGION || 'eu-central-1' }}
-        run: |
-          AMI_ID=$(jq -r '.builds[] | select(.builder_type=="amazon-ebs") | .artifact_id' packer-manifest.json | tail -1 | cut -d: -f2)
-          {
-            echo "## AWS AMI (${{ matrix.arch }})"
-            echo "- Region: \`${REGION}\`"
-            echo "- Instance type: \`${{ matrix.instance_type }}\`"
-            echo "- AMI ID: \`${AMI_ID}\`"
-          } >> "$GITHUB_STEP_SUMMARY"

+ 2 - 14
.github/workflows/smoke.yml

@@ -1,7 +1,7 @@
 name: Deploy Smoke Tests
 name: Deploy Smoke Tests
 
 
-# Container smoke tests for the unattended install path and first-boot
-# credential generation. Runs only when the install/deploy assets change.
+# Container smoke test for the unattended (cloud-init) install path.
+# Runs only when the install/deploy assets change.
 
 
 on:
 on:
   push:
   push:
@@ -30,15 +30,3 @@ jobs:
       - uses: actions/checkout@v7
       - uses: actions/checkout@v7
       - name: Non-interactive install smoke test
       - name: Non-interactive install smoke test
         run: bash deploy/test/smoke-noninteractive.sh
         run: bash deploy/test/smoke-noninteractive.sh
-
-  first-boot:
-    strategy:
-      fail-fast: false
-      matrix:
-        runner: [ubuntu-latest, ubuntu-24.04-arm]
-    runs-on: ${{ matrix.runner }}
-    timeout-minutes: 15
-    steps:
-      - uses: actions/checkout@v7
-      - name: First-boot credential smoke test
-        run: bash deploy/test/smoke-firstboot.sh

+ 3 - 5
README.ar_EG.md

@@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
 
 
 للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki).
 للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki).
 
 
-### التثبيت غير التفاعلي وصور السحابة
+### التثبيت غير التفاعلي
 
 
-يعمل المثبِّت أيضًا **بشكل غير تفاعلي** لـ cloud-init والصور الجاهزة (golden images).
+يعمل المثبِّت أيضًا **بشكل غير تفاعلي** لـ cloud-init.
 عيّن `XUI_NONINTERACTIVE=1` (أو مرّره عبر أنبوب دون TTY) وسيتولى التثبيت من البداية إلى النهاية
 عيّن `XUI_NONINTERACTIVE=1` (أو مرّره عبر أنبوب دون TTY) وسيتولى التثبيت من البداية إلى النهاية
 دون أي مطالبات، مُنشئًا بيانات اعتماد عشوائية وكاتبًا إياها في
 دون أي مطالبات، مُنشئًا بيانات اعتماد عشوائية وكاتبًا إياها في
 `/etc/x-ui/install-result.env`. راجع [`deploy/`](deploy/) لـ:
 `/etc/x-ui/install-result.env`. راجع [`deploy/`](deploy/) لـ:
 
 
 - [بيانات مستخدم cloud-init](deploy/cloud-init/) — تثبيت غير تفاعلي على أي سحابة (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [بيانات مستخدم cloud-init](deploy/cloud-init/) — تثبيت غير تفاعلي على أي سحابة (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [صورة Packer الجاهزة](deploy/packer/) — بناء صورة AWS EC2 AMI و qcow2 (amd64/arm64) مع بيانات اعتماد لكل نسخة يتم إنشاؤها عند الإقلاع الأول
-- [Amazon Lightsail](deploy/lightsail/) — سكربت إطلاق وأداة بناء لقطات قابلة لإعادة الاستخدام
-- [قائمة تحقق AWS Marketplace](deploy/marketplace/aws/)
+- [ملاحظات Hetzner Cloud](deploy/marketplace/hetzner/) — نشر يعتمد على cloud-init على Hetzner
 
 
 ## المنصات المدعومة
 ## المنصات المدعومة
 
 

+ 3 - 5
README.es_ES.md

@@ -89,17 +89,15 @@ Durante la instalación se generan un nombre de usuario, una contraseña y una r
 
 
 Para la documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki).
 Para la documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki).
 
 
-### Instalación desatendida e imágenes de nube
+### Instalación desatendida
 
 
-El instalador también se ejecuta de forma **no interactiva** para cloud-init e imágenes doradas (golden images).
+El instalador también se ejecuta de forma **no interactiva** para cloud-init.
 Define `XUI_NONINTERACTIVE=1` (o canalízalo sin TTY) y realizará la instalación de principio a fin sin
 Define `XUI_NONINTERACTIVE=1` (o canalízalo sin TTY) y realizará la instalación de principio a fin sin
 ninguna pregunta, generando credenciales aleatorias y escribiéndolas en
 ninguna pregunta, generando credenciales aleatorias y escribiéndolas en
 `/etc/x-ui/install-result.env`. Consulta [`deploy/`](deploy/) para:
 `/etc/x-ui/install-result.env`. Consulta [`deploy/`](deploy/) para:
 
 
 - [User-data de cloud-init](deploy/cloud-init/) — instalación desatendida en cualquier nube (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [User-data de cloud-init](deploy/cloud-init/) — instalación desatendida en cualquier nube (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [Imagen dorada de Packer](deploy/packer/) — crea una AMI de AWS EC2 + qcow2 (amd64/arm64) con credenciales por instancia generadas en el primer arranque
-- [Amazon Lightsail](deploy/lightsail/) — script de lanzamiento + constructor de snapshots reutilizable
-- [Lista de verificación de AWS Marketplace](deploy/marketplace/aws/)
+- [Notas de Hetzner Cloud](deploy/marketplace/hetzner/) — despliegue basado en cloud-init en Hetzner
 
 
 ## Plataformas Compatibles
 ## Plataformas Compatibles
 
 

+ 3 - 5
README.fa_IR.md

@@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
 
 
 برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید.
 برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید.
 
 
-### نصب بدون نظارت و ایمیج‌های ابری
+### نصب بدون نظارت
 
 
-نصب‌کننده به‌صورت **غیرتعاملی** نیز برای cloud-init و ایمیج‌های آماده (golden images) اجرا می‌شود.
+نصب‌کننده به‌صورت **غیرتعاملی** نیز برای cloud-init اجرا می‌شود.
 ‏`XUI_NONINTERACTIVE=1` را تنظیم کنید (یا بدون TTY از طریق pipe اجرا کنید) تا نصب به‌صورت سرتاسری و بدون
 ‏`XUI_NONINTERACTIVE=1` را تنظیم کنید (یا بدون TTY از طریق pipe اجرا کنید) تا نصب به‌صورت سرتاسری و بدون
 هیچ پرسشی انجام شود، اطلاعات ورود تصادفی تولید کرده و آن‌ها را در
 هیچ پرسشی انجام شود، اطلاعات ورود تصادفی تولید کرده و آن‌ها را در
 `/etc/x-ui/install-result.env` می‌نویسد. برای موارد زیر به [`deploy/`](deploy/) مراجعه کنید:
 `/etc/x-ui/install-result.env` می‌نویسد. برای موارد زیر به [`deploy/`](deploy/) مراجعه کنید:
 
 
 - [user-data مربوط به Cloud-init](deploy/cloud-init/) — نصب بدون نظارت روی هر ابری (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [user-data مربوط به Cloud-init](deploy/cloud-init/) — نصب بدون نظارت روی هر ابری (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [ایمیج آماده‌ی Packer](deploy/packer/) — ساخت یک AMI برای AWS EC2 به‌همراه qcow2 (amd64/arm64) با اطلاعات ورودِ مخصوص هر اینستنس که در نخستین بوت تولید می‌شود
-- [Amazon Lightsail](deploy/lightsail/) — اسکریپت راه‌اندازی به‌همراه سازنده‌ی اسنپ‌شات قابل‌استفاده‌ی مجدد
-- [چک‌لیست AWS Marketplace](deploy/marketplace/aws/)
+- [یادداشت‌های Hetzner Cloud](deploy/marketplace/hetzner/) — استقرار مبتنی بر cloud-init روی Hetzner
 
 
 ## پلتفرم‌های پشتیبانی‌شده
 ## پلتفرم‌های پشتیبانی‌شده
 
 

+ 3 - 5
README.md

@@ -89,17 +89,15 @@ During installation a random username, password, and access path are generated.
 
 
 For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki).
 For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki).
 
 
-### Unattended install & cloud images
+### Unattended install
 
 
-The installer also runs **non-interactively** for cloud-init and golden images.
+The installer also runs **non-interactively** for cloud-init.
 Set `XUI_NONINTERACTIVE=1` (or pipe with no TTY) and it installs end-to-end with
 Set `XUI_NONINTERACTIVE=1` (or pipe with no TTY) and it installs end-to-end with
 zero prompts, generating random credentials and writing them to
 zero prompts, generating random credentials and writing them to
 `/etc/x-ui/install-result.env`. See [`deploy/`](deploy/) for:
 `/etc/x-ui/install-result.env`. See [`deploy/`](deploy/) for:
 
 
 - [Cloud-init user-data](deploy/cloud-init/) — unattended install on any cloud (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [Cloud-init user-data](deploy/cloud-init/) — unattended install on any cloud (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [Packer golden image](deploy/packer/) — build an AWS EC2 AMI + qcow2 (amd64/arm64) with per-instance credentials generated on first boot
-- [Amazon Lightsail](deploy/lightsail/) — launch script + reusable snapshot builder
-- [AWS Marketplace checklist](deploy/marketplace/aws/)
+- [Hetzner Cloud notes](deploy/marketplace/hetzner/) — cloud-init deployment on Hetzner
 
 
 ## Supported Platforms
 ## Supported Platforms
 
 

+ 3 - 5
README.ru_RU.md

@@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
 
 
 Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki).
 Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki).
 
 
-### Автоматическая установка и облачные образы
+### Автоматическая установка
 
 
-Установщик также работает в **неинтерактивном** режиме для cloud-init и готовых образов.
+Установщик также работает в **неинтерактивном** режиме для cloud-init.
 Задайте `XUI_NONINTERACTIVE=1` (или передайте по конвейеру без TTY), и установка пройдёт от начала до конца
 Задайте `XUI_NONINTERACTIVE=1` (или передайте по конвейеру без TTY), и установка пройдёт от начала до конца
 без единого запроса: будут сгенерированы случайные учётные данные и записаны в
 без единого запроса: будут сгенерированы случайные учётные данные и записаны в
 `/etc/x-ui/install-result.env`. Смотрите [`deploy/`](deploy/) для:
 `/etc/x-ui/install-result.env`. Смотрите [`deploy/`](deploy/) для:
 
 
 - [Cloud-init user-data](deploy/cloud-init/) — автоматическая установка в любом облаке (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [Cloud-init user-data](deploy/cloud-init/) — автоматическая установка в любом облаке (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [Готовый образ Packer](deploy/packer/) — сборка AWS EC2 AMI + qcow2 (amd64/arm64) с учётными данными для каждого экземпляра, генерируемыми при первой загрузке
-- [Amazon Lightsail](deploy/lightsail/) — скрипт запуска + переиспользуемый сборщик снимков
-- [Чек-лист для AWS Marketplace](deploy/marketplace/aws/)
+- [Заметки по Hetzner Cloud](deploy/marketplace/hetzner/) — развёртывание на Hetzner на базе cloud-init
 
 
 ## Поддерживаемые платформы
 ## Поддерживаемые платформы
 
 

+ 3 - 5
README.tr_TR.md

@@ -89,17 +89,15 @@ Kurulum sırasında rastgele bir kullanıcı adı, şifre ve erişim yolu oluşt
 
 
 Tam dokümantasyon için lütfen [proje Wiki sayfasını](https://github.com/MHSanaei/3x-ui/wiki) ziyaret edin.
 Tam dokümantasyon için lütfen [proje Wiki sayfasını](https://github.com/MHSanaei/3x-ui/wiki) ziyaret edin.
 
 
-### Etkileşimsiz kurulum ve hazır bulut imajları
+### Etkileşimsiz kurulum
 
 
-Yükleyici, cloud-init ve hazır (golden) imajlar için **etkileşimsiz** olarak da çalışır.
+Yükleyici, cloud-init için **etkileşimsiz** olarak da çalışır.
 `XUI_NONINTERACTIVE=1` ayarlayın (veya TTY olmadan boru hattına aktarın); kurulum baştan
 `XUI_NONINTERACTIVE=1` ayarlayın (veya TTY olmadan boru hattına aktarın); kurulum baştan
 sona hiçbir soru sormadan tamamlanır, rastgele kimlik bilgileri oluşturup bunları
 sona hiçbir soru sormadan tamamlanır, rastgele kimlik bilgileri oluşturup bunları
 `/etc/x-ui/install-result.env` dosyasına yazar. Şunlar için [`deploy/`](deploy/) klasörüne bakın:
 `/etc/x-ui/install-result.env` dosyasına yazar. Şunlar için [`deploy/`](deploy/) klasörüne bakın:
 
 
 - [Cloud-init user-data](deploy/cloud-init/) — herhangi bir bulutta etkileşimsiz kurulum (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [Cloud-init user-data](deploy/cloud-init/) — herhangi bir bulutta etkileşimsiz kurulum (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [Packer hazır imajı](deploy/packer/) — ilk açılışta her örnek (instance) için kimlik bilgileri oluşturan bir AWS EC2 AMI + qcow2 (amd64/arm64) imajı oluşturun
-- [Amazon Lightsail](deploy/lightsail/) — başlatma betiği + yeniden kullanılabilir anlık görüntü (snapshot) oluşturucu
-- [AWS Marketplace kontrol listesi](deploy/marketplace/aws/)
+- [Hetzner Cloud notları](deploy/marketplace/hetzner/) — Hetzner üzerinde cloud-init tabanlı dağıtım
 
 
 ## Desteklenen Platformlar
 ## Desteklenen Platformlar
 
 

+ 3 - 5
README.zh_CN.md

@@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
 
 
 完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。
 完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。
 
 
-### 无人值守安装与云镜像
+### 无人值守安装
 
 
-安装程序也可以**非交互式**运行,适用于 cloud-init 和黄金镜像(golden image)
+安装程序也可以**非交互式**运行,适用于 cloud-init。
 设置 `XUI_NONINTERACTIVE=1`(或在无 TTY 的情况下通过管道传入),它就会全程
 设置 `XUI_NONINTERACTIVE=1`(或在无 TTY 的情况下通过管道传入),它就会全程
 零提示地完成端到端安装,生成随机凭据并写入
 零提示地完成端到端安装,生成随机凭据并写入
 `/etc/x-ui/install-result.env`。请参阅 [`deploy/`](deploy/):
 `/etc/x-ui/install-result.env`。请参阅 [`deploy/`](deploy/):
 
 
 - [Cloud-init user-data](deploy/cloud-init/) — 在任意云平台上无人值守安装(Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
 - [Cloud-init user-data](deploy/cloud-init/) — 在任意云平台上无人值守安装(Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
-- [Packer golden image](deploy/packer/) — 构建 AWS EC2 AMI + qcow2(amd64/arm64),首次启动时生成每个实例独有的凭据
-- [Amazon Lightsail](deploy/lightsail/) — 启动脚本 + 可复用的快照构建器
-- [AWS Marketplace 清单](deploy/marketplace/aws/)
+- [Hetzner Cloud 说明](deploy/marketplace/hetzner/) — 在 Hetzner 上基于 cloud-init 的部署
 
 
 ## 支持的平台
 ## 支持的平台
 
 

+ 9 - 16
deploy/README.md

@@ -1,27 +1,20 @@
-# Cloud deployment & golden images
+# Cloud deployment (unattended install)
 
 
-Tooling to ship the 3x-ui panel as a cloud image or via unattended install,
-with **per-instance credentials generated on first boot** (never `admin/admin`,
-never a shared session secret). Everything here supports **amd64 and arm64**.
+Tooling to ship the 3x-ui panel via unattended install, with **per-instance
+credentials generated on first boot** (never `admin/admin`, never a shared
+session secret). Works on amd64 and arm64.
 
 
 | Path | What it is | Use when |
 | Path | What it is | Use when |
 | --- | --- | --- |
 | --- | --- | --- |
 | [`cloud-init/`](cloud-init/) | Generic cloud-init user-data (unattended `install.sh`) | Any cloud, no image build |
 | [`cloud-init/`](cloud-init/) | Generic cloud-init user-data (unattended `install.sh`) | Any cloud, no image build |
-| [`packer/`](packer/) | Packer build → AWS AMI + qcow2/raw | Reusable / Marketplace images |
-| [`lightsail/`](lightsail/) | Launch script + snapshot builder | Amazon Lightsail |
-| [`firstboot/`](firstboot/) | First-boot unit + script that mints per-instance creds | Used by the Packer/Lightsail images |
-| [`marketplace/aws/`](marketplace/aws/) | AWS Marketplace submission checklist | Publishing an EC2 AMI |
 | [`marketplace/hetzner/`](marketplace/hetzner/) | Hetzner Cloud notes | Hetzner deployments |
 | [`marketplace/hetzner/`](marketplace/hetzner/) | Hetzner Cloud notes | Hetzner deployments |
-| [`test/`](test/) | Container smoke tests | Verifying the install/firstboot paths |
+| [`test/`](test/) | Container smoke test | Verifying the install path |
 
 
-## Two models
+## How it works
 
 
-- **Non-interactive install (cloud-init):** `install.sh` runs unattended when
-  `XUI_NONINTERACTIVE=1` or stdin is not a TTY. Each instance installs and
-  configures itself with random credentials. See [`cloud-init/README.md`](cloud-init/README.md).
-- **Golden image (Packer):** the image contains the panel but **no DB and no
-  secrets**; `firstboot` generates unique credentials on first boot. See
-  [`packer/README.md`](packer/README.md).
+`install.sh` runs unattended when `XUI_NONINTERACTIVE=1` or stdin is not a TTY.
+Each instance installs and configures itself with random credentials. See
+[`cloud-init/README.md`](cloud-init/README.md).
 
 
 ## Unattended install knobs
 ## Unattended install knobs
 
 

+ 4 - 9
deploy/cloud-init/README.md

@@ -1,12 +1,8 @@
-# 3x-ui via cloud-init (generic, no golden image)
+# 3x-ui via cloud-init
 
 
-This is the **secondary** deployment path: a single [`cloud-init.yaml`](cloud-init.yaml)
-user-data file that installs 3x-ui non-interactively on a fresh Ubuntu/Debian
-VM and generates **unique random credentials per instance**. Use it when you do
-not want to build a golden image — it works on any cloud-init platform.
-
-> For AWS Marketplace / reusable images, use the Packer build in
-> [`../packer/`](../packer/) instead.
+A single [`cloud-init.yaml`](cloud-init.yaml) user-data file that installs 3x-ui
+non-interactively on a fresh Ubuntu/Debian VM and generates **unique random
+credentials per instance**. It works on any cloud-init platform.
 
 
 ## How it works
 ## How it works
 
 
@@ -53,7 +49,6 @@ Edit the `export XUI_*` lines inside the `write_files` block of
   `hcloud server create --image ubuntu-24.04 --user-data-from-file cloud-init.yaml ...`
   `hcloud server create --image ubuntu-24.04 --user-data-from-file cloud-init.yaml ...`
 - **AWS EC2** — *Advanced details → User data*: paste the file. Or
 - **AWS EC2** — *Advanced details → User data*: paste the file. Or
   `aws ec2 run-instances --user-data file://cloud-init.yaml ...`
   `aws ec2 run-instances --user-data file://cloud-init.yaml ...`
-  (For a reusable Marketplace image use the Packer AMI build instead.)
 - **DigitalOcean** — *Create Droplet → Advanced options → Add Initialization
 - **DigitalOcean** — *Create Droplet → Advanced options → Add Initialization
   scripts (user data)*: paste the file. Or `doctl compute droplet create --user-data-file cloud-init.yaml ...`
   scripts (user data)*: paste the file. Or `doctl compute droplet create --user-data-file cloud-init.yaml ...`
 - **Vultr** — *Deploy → Additional Features → Cloud-Init User-Data*: paste the file.
 - **Vultr** — *Deploy → Additional Features → Cloud-Init User-Data*: paste the file.

+ 0 - 22
deploy/firstboot/x-ui-firstboot.service

@@ -1,22 +0,0 @@
-[Unit]
-Description=3x-ui first-boot per-instance credential generation
-Documentation=https://github.com/MHSanaei/3x-ui
-# Run after the network and cloud-init are up, but BEFORE the panel starts, so
-# the panel never serves the default admin/admin account.
-After=network-online.target cloud-init.service
-Wants=network-online.target
-Before=x-ui.service
-# Skip entirely once the sentinel exists (cheap guard; the script re-checks too).
-ConditionPathExists=!/etc/x-ui/.firstboot-done
-
-[Service]
-Type=oneshot
-RemainAfterExit=yes
-# Inherit the same DB configuration the panel uses (sqlite default / postgres).
-EnvironmentFile=-/etc/default/x-ui
-EnvironmentFile=-/etc/conf.d/x-ui
-EnvironmentFile=-/etc/sysconfig/x-ui
-ExecStart=/usr/local/x-ui/x-ui-firstboot.sh
-
-[Install]
-WantedBy=multi-user.target

+ 0 - 166
deploy/firstboot/x-ui-firstboot.sh

@@ -1,166 +0,0 @@
-#!/usr/bin/env bash
-#
-# x-ui-firstboot.sh — generate per-instance 3x-ui panel credentials on first boot.
-#
-# A golden image (AMI / qcow2) MUST ship without an initialized x-ui.db: the
-# panel seeds a hardcoded admin/admin user and generates its session secret +
-# panel GUID on first start, so a baked DB would make every clone share the same
-# credentials and secret. This script runs ONCE, before x-ui.service starts, and
-# replaces the default admin with fresh random credentials on a random high port.
-#
-# Idempotent: a sentinel file guards against re-running. If a non-default admin
-# already exists (operator pre-configured the box), regeneration is skipped.
-#
-# Wired up by deploy/packer/scripts/provision.sh; ordered Before=x-ui.service.
-
-set -u
-
-SENTINEL="/etc/x-ui/.firstboot-done"
-CRED_FILE="/etc/x-ui/credentials.txt"
-MOTD_FILE="/etc/motd"
-XUI_DIR="${XUI_MAIN_FOLDER:-/usr/local/x-ui}"
-XUI_BIN="${XUI_DIR}/x-ui"
-
-log() { echo "[x-ui-firstboot] $*"; }
-
-# Already provisioned — nothing to do (idempotent on re-run / re-image).
-if [ -f "$SENTINEL" ]; then
-    log "sentinel $SENTINEL present; skipping."
-    exit 0
-fi
-
-if [ ! -x "$XUI_BIN" ]; then
-    log "ERROR: x-ui binary not found at $XUI_BIN"
-    exit 1
-fi
-
-# Inherit DB configuration (sqlite default; postgres via XUI_DB_TYPE/XUI_DB_DSN)
-# from the same env files the systemd unit loads, so the binary talks to the
-# same database the panel will use.
-for ef in /etc/default/x-ui /etc/conf.d/x-ui /etc/sysconfig/x-ui; do
-    if [ -r "$ef" ]; then
-        set -a
-        # shellcheck disable=SC1090
-        . "$ef"
-        set +a
-    fi
-done
-
-install -d -m 755 /etc/x-ui 2> /dev/null || true
-
-# Defense-in-depth: make sure the panel is not running while we mutate the DB.
-if command -v systemctl > /dev/null 2>&1; then
-    systemctl stop x-ui > /dev/null 2>&1 || true
-fi
-
-gen_random_string() {
-    local length="$1"
-    openssl rand -base64 $((length * 2)) | tr -dc 'a-zA-Z0-9' | head -c "$length"
-}
-
-# Best-effort public IPv4 for the displayed access URL (cosmetic only — the
-# panel binds 0.0.0.0). Falls back to the primary local IP, then a placeholder.
-detect_ip() {
-    local ip=""
-    local url
-    for url in https://api4.ipify.org https://ipv4.icanhazip.com https://4.ident.me; do
-        ip=$(curl -fsS4 --max-time 3 "$url" 2> /dev/null | tr -d '[:space:]')
-        if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
-            echo "$ip"
-            return 0
-        fi
-    done
-    ip=$(hostname -I 2> /dev/null | awk '{print $1}')
-    if [ -n "$ip" ]; then
-        echo "$ip"
-        return 0
-    fi
-    echo "<server-ip>"
-}
-
-# Detect whether the seeded admin/admin default is still in place.
-default_creds=$("$XUI_BIN" setting -show true 2> /dev/null | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
-
-# The parse MUST yield exactly "true" or "false". If the command failed or its
-# output format changed, refuse to proceed: do NOT write the sentinel, so the
-# next boot retries instead of silently leaving admin/admin in place.
-if [ "$default_creds" != "true" ] && [ "$default_creds" != "false" ]; then
-    log "ERROR: could not determine credential state (hasDefaultCredential='${default_creds}'); not writing sentinel, will retry next boot."
-    exit 1
-fi
-
-if [ "$default_creds" = "false" ]; then
-    log "non-default admin already configured; skipping credential regeneration."
-    {
-        echo "3x-ui first-boot: a non-default admin account already exists on this"
-        echo "instance, so credentials were left unchanged."
-    } > "$MOTD_FILE" 2> /dev/null || true
-    : > "$SENTINEL" 2> /dev/null || true
-    chmod 600 "$SENTINEL" 2> /dev/null || true
-    exit 0
-fi
-
-log "generating per-instance credentials..."
-
-NEW_USER="${XUI_USERNAME:-$(gen_random_string 10)}"
-NEW_PASS="${XUI_PASSWORD:-$(gen_random_string 16)}"
-NEW_PATH="${XUI_WEB_BASE_PATH:-$(gen_random_string 18)}"
-NEW_PORT="${XUI_PANEL_PORT:-$(shuf -i 1024-62000 -n 1)}"
-
-# Clean settings slate: drops any baked port/webBasePath and forces the panel
-# to regenerate its session secret + panel GUID on next start (per-instance).
-"$XUI_BIN" setting -reset > /dev/null 2>&1 || true
-
-# Apply fresh random identity. UpdateFirstUser renames the seeded admin row and
-# rehashes the password, so admin/admin no longer exists after this call.
-if ! "$XUI_BIN" setting -username "$NEW_USER" -password "$NEW_PASS" -port "$NEW_PORT" -webBasePath "$NEW_PATH" > /dev/null 2>&1; then
-    log "ERROR: failed to apply new panel settings."
-    exit 1
-fi
-
-API_TOKEN=$("$XUI_BIN" setting -getApiToken true 2> /dev/null | grep -Eo 'apiToken: .+' | awk '{print $2}')
-SERVER_IP=$(detect_ip)
-ACCESS_URL="http://${SERVER_IP}:${NEW_PORT}/${NEW_PATH}"
-
-# Persist credentials for the operator (root-only). Values are shell-escaped
-# with %q so the file stays safe to `source` even if a value contains shell
-# metacharacters (the smoke test and operators source this file).
-umask 077
-{
-    echo "# 3x-ui per-instance credentials (generated on first boot)"
-    printf 'XUI_USERNAME=%q\n' "$NEW_USER"
-    printf 'XUI_PASSWORD=%q\n' "$NEW_PASS"
-    printf 'XUI_PANEL_PORT=%q\n' "$NEW_PORT"
-    printf 'XUI_WEB_BASE_PATH=%q\n' "$NEW_PATH"
-    printf 'XUI_ACCESS_URL=%q\n' "$ACCESS_URL"
-    printf 'XUI_API_TOKEN=%q\n' "$API_TOKEN"
-} > "$CRED_FILE"
-chmod 600 "$CRED_FILE" 2> /dev/null || true
-
-# Friendly login banner shown on SSH / console before the panel is reachable.
-# /etc/motd is world-readable, so it MUST NOT contain the password or API token;
-# those secrets live only in ${CRED_FILE} (mode 600). Show non-secret info only.
-cat > "$MOTD_FILE" 2> /dev/null << EOF
-
-========================================================================
-  3x-ui panel — per-instance credentials (generated on first boot)
-========================================================================
-  Access URL : ${ACCESS_URL}
-  Username   : ${NEW_USER}
-
-  The password and API token are NOT shown here (this banner is
-  world-readable). Read them as root with:
-      sudo cat ${CRED_FILE}
-
-  Change the password after login. If no public IP is shown above,
-  replace <server-ip> with the address you reach this server on.
-========================================================================
-
-EOF
-
-# Mark complete so we never regenerate on subsequent boots.
-: > "$SENTINEL" 2> /dev/null || true
-chmod 600 "$SENTINEL" 2> /dev/null || true
-
-log "done. Panel will start on port ${NEW_PORT} with a unique admin account."
-exit 0

+ 0 - 94
deploy/lightsail/README.md

@@ -1,94 +0,0 @@
-# 3x-ui on Amazon Lightsail
-
-Two self-service ways to run 3x-ui on Lightsail, both producing **unique
-per-instance credentials** (never `admin/admin`, never a shared secret).
-
-> **Reality check.** The Lightsail *blueprint* list (WordPress, LAMP, GitLab…)
-> is curated by AWS — you **cannot** self-publish your panel there, and Lightsail
-> **cannot** launch from an arbitrary EC2 AMI. What you *can* do yourself is the
-> two paths below. (For a public AWS listing you'd use the EC2 **AMI** +
-> Marketplace path in [`../marketplace/aws/`](../marketplace/aws/), which is a
-> different product from Lightsail.)
-
----
-
-## Path A — launch script (simplest, self-service)
-
-Install on a fresh instance at creation time. No image to build.
-
-1. **Create instance** → platform **Linux/Unix** → blueprint **OS Only → Ubuntu 24.04**.
-2. **Add launch script** → paste [`launch-script.sh`](launch-script.sh).
-3. Create the instance.
-4. After it boots, read the credentials:
-   ```bash
-   ssh ubuntu@<public-ip> 'sudo cat /etc/x-ui/install-result.env'
-   ```
-5. **Open the panel port** (see the firewall note below) and log in.
-
-CLI equivalent:
-
-```bash
-aws lightsail create-instances \
-  --instance-names my-3xui \
-  --availability-zone eu-central-1a \
-  --blueprint-id ubuntu_24_04 \
-  --bundle-id small_3_0 \
-  --user-data file://deploy/lightsail/launch-script.sh \
-  --region eu-central-1
-```
-
-By default the panel uses a **random** high port (in `install-result.env`). To
-pin a known port so you can pre-open it, set `export XUI_PANEL_PORT=54321` inside
-`launch-script.sh`.
-
----
-
-## Path B — reusable snapshot (your own "ready image")
-
-Build a Lightsail **snapshot** once; launch as many instances from it as you
-like, each generating its own credentials on first boot (the golden-image model).
-
-```bash
-deploy/lightsail/build-snapshot.sh --region eu-central-1 --panel-port 54321
-```
-
-What it does: launches a temporary Ubuntu instance with
-[`snapshot-userdata.sh`](snapshot-userdata.sh) (installs the panel, **no DB**,
-enables the first-boot unit), strips all state via the shared
-[`cleanup.sh`](../packer/scripts/cleanup.sh), then snapshots and deletes the
-build instance. Requires `awscli`, `jq`, `ssh` and Lightsail permissions.
-
-Launch instances from the snapshot:
-
-```bash
-aws lightsail create-instances-from-snapshot \
-  --instance-snapshot-name 3x-ui-ubuntu-24.04-<stamp> \
-  --instance-names my-3xui-1 --bundle-id small_3_0 \
-  --availability-zone eu-central-1a --region eu-central-1
-```
-
-Each launched instance runs `x-ui-firstboot` and writes its unique credentials to
-`/etc/x-ui/credentials.txt` + `/etc/motd`. With `--panel-port` the port is the
-same across instances (only the credentials differ), so you can pre-open it.
-
-> Lightsail snapshots are **private to your AWS account** (and region). To use one
-> elsewhere you can export it to EC2 (`aws lightsail export-snapshot`) and share
-> the resulting AMI.
-
----
-
-## Lightsail firewall note (important)
-
-Lightsail's per-instance firewall only opens **22 / 80 / 443** by default. The
-panel runs on a different port, so you must open it:
-
-- Console: instance → **Networking → IPv4 Firewall → Add rule** (TCP, the panel port).
-- CLI:
-  ```bash
-  aws lightsail open-instance-public-ports --region eu-central-1 \
-    --instance-name my-3xui \
-    --port-info fromPort=54321,toPort=54321,protocol=TCP
-  ```
-
-The panel port is in `/etc/x-ui/install-result.env` (Path A) or
-`/etc/x-ui/credentials.txt` (Path B), or fixed via `--panel-port` / `XUI_PANEL_PORT`.

+ 0 - 192
deploy/lightsail/build-snapshot.sh

@@ -1,192 +0,0 @@
-#!/usr/bin/env bash
-#
-# build-snapshot.sh — build a reusable Amazon Lightsail snapshot of 3x-ui.
-#
-# Flow (mirrors the Packer golden-image model, via the Lightsail API):
-#   1. create an Ubuntu Lightsail instance with snapshot-userdata.sh
-#      (installs the panel, NO database, enables the first-boot unit)
-#   2. wait for provisioning, then (optionally) pin a known panel port and run
-#      the shared cleanup.sh (wipes any DB/creds/keys/host-keys/cloud-init state)
-#   3. stop the instance and create an instance snapshot
-#   4. delete the build instance (unless --keep-instance)
-#
-# Every instance you later launch from the snapshot generates its OWN unique
-# credentials on first boot (see deploy/firstboot/). The snapshot is private to
-# your AWS account.
-#
-# Requirements: awscli v2, jq, ssh. AWS credentials with Lightsail permissions.
-# Usage:
-#   deploy/lightsail/build-snapshot.sh --region eu-central-1 [options]
-# Options:
-#   --region <r>            AWS region (default: $AWS_REGION or eu-central-1)
-#   --blueprint-id <id>     Lightsail blueprint (default: ubuntu_24_04)
-#   --bundle-id <id>        Lightsail bundle/size (default: small_3_0)
-#   --availability-zone <z> AZ (default: <region>a)
-#   --panel-port <p>        Pin the panel port in the snapshot so you can pre-open
-#                           it in the Lightsail firewall (default: random per instance)
-#   --snapshot-name <n>     Snapshot name (default: 3x-ui-ubuntu-24.04-<timestamp>)
-#   --keep-instance         Do not delete the build instance afterwards
-set -euo pipefail
-
-REGION="${AWS_REGION:-eu-central-1}"
-BLUEPRINT="ubuntu_24_04"
-BUNDLE="small_3_0"
-AZ=""
-PANEL_PORT=""
-SNAPSHOT_NAME=""
-KEEP_INSTANCE=0
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-STAMP="$(date +%Y%m%d-%H%M%S)"
-INSTANCE_NAME="3xui-build-${STAMP}"
-KEY_FILE=""
-
-log() { echo "[build-snapshot] $*"; }
-die() {
-    echo "[build-snapshot] ERROR: $*" >&2
-    exit 1
-}
-
-while [ $# -gt 0 ]; do
-    case "$1" in
-        --region) REGION="$2"; shift 2 ;;
-        --blueprint-id) BLUEPRINT="$2"; shift 2 ;;
-        --bundle-id) BUNDLE="$2"; shift 2 ;;
-        --availability-zone) AZ="$2"; shift 2 ;;
-        --panel-port) PANEL_PORT="$2"; shift 2 ;;
-        --snapshot-name) SNAPSHOT_NAME="$2"; shift 2 ;;
-        --keep-instance) KEEP_INSTANCE=1; shift ;;
-        -h | --help) sed -n '2,40p' "$0"; exit 0 ;;
-        *) die "unknown option: $1" ;;
-    esac
-done
-
-[ -n "$AZ" ] || AZ="${REGION}a"
-[ -n "$SNAPSHOT_NAME" ] || SNAPSHOT_NAME="3x-ui-ubuntu-24.04-${STAMP}"
-
-for cmd in aws jq ssh; do
-    command -v "$cmd" > /dev/null 2>&1 || die "'$cmd' is required"
-done
-
-SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR)
-
-cleanup() {
-    [ -n "$KEY_FILE" ] && rm -f "$KEY_FILE"
-    if [ "$KEEP_INSTANCE" -eq 0 ]; then
-        aws lightsail delete-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null 2>&1 || true
-    fi
-}
-trap cleanup EXIT
-
-wait_state() {
-    local want="$1" tries="${2:-60}" st
-    for _ in $(seq 1 "$tries"); do
-        st=$(aws lightsail get-instance-state --instance-name "$INSTANCE_NAME" --region "$REGION" \
-            --query 'state.name' --output text 2> /dev/null || echo "")
-        [ "$st" = "$want" ] && return 0
-        sleep 5
-    done
-    return 1
-}
-
-log "creating build instance ${INSTANCE_NAME} (${BLUEPRINT}/${BUNDLE}) in ${REGION}..."
-aws lightsail create-instances \
-    --instance-names "$INSTANCE_NAME" \
-    --availability-zone "$AZ" \
-    --blueprint-id "$BLUEPRINT" \
-    --bundle-id "$BUNDLE" \
-    --user-data "file://${SCRIPT_DIR}/snapshot-userdata.sh" \
-    --region "$REGION" > /dev/null
-
-log "waiting for instance to run..."
-wait_state running 60 || die "instance did not reach 'running'"
-
-IP=$(aws lightsail get-instance --instance-name "$INSTANCE_NAME" --region "$REGION" \
-    --query 'instance.publicIpAddress' --output text)
-if [ -z "$IP" ] || [ "$IP" = "None" ]; then die "no public IP"; fi
-log "instance IP: ${IP}"
-
-KEY_FILE="$(mktemp)"
-# download-default-key-pair returns the key in 'privateKeyBase64'. Despite the
-# name, the CLI historically emits the plaintext PEM (-----BEGIN...); the API
-# docs describe it as base64. Handle both: write PEM as-is, else base64-decode.
-KEY_RAW="$(aws lightsail download-default-key-pair --region "$REGION" \
-    --query 'privateKeyBase64' --output text)"
-[ -n "$KEY_RAW" ] && [ "$KEY_RAW" != "None" ] || die "failed to download default key pair"
-case "$KEY_RAW" in
-    *-----BEGIN*) printf '%s\n' "$KEY_RAW" > "$KEY_FILE" ;;
-    *) printf '%s' "$KEY_RAW" | base64 -d > "$KEY_FILE" 2> /dev/null \
-        || die "private key is neither PEM nor valid base64" ;;
-esac
-grep -q -- "-----BEGIN" "$KEY_FILE" || die "downloaded key is not a valid PEM private key"
-chmod 600 "$KEY_FILE"
-
-log "waiting for provisioning to finish (this installs the panel)..."
-ok=0
-for _ in $(seq 1 72); do # ~12 min
-    if ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
-        'test -f /var/lib/3xui-provision-done' 2> /dev/null; then
-        ok=1
-        break
-    fi
-    sleep 10
-done
-[ "$ok" -eq 1 ] || die "provisioning did not complete in time"
-log "provisioning complete."
-
-if [ -n "$PANEL_PORT" ]; then
-    log "pinning panel port ${PANEL_PORT} (username/password stay random)..."
-    ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
-        "echo 'XUI_PANEL_PORT=${PANEL_PORT}' | sudo tee -a /etc/default/x-ui >/dev/null"
-fi
-
-log "stripping instance state (shared cleanup.sh)..."
-ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
-    'curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/deploy/packer/scripts/cleanup.sh | sudo bash'
-
-log "stopping instance..."
-aws lightsail stop-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null
-wait_state stopped 60 || die "instance did not stop"
-
-log "creating snapshot ${SNAPSHOT_NAME}..."
-aws lightsail create-instance-snapshot \
-    --instance-name "$INSTANCE_NAME" \
-    --instance-snapshot-name "$SNAPSHOT_NAME" \
-    --region "$REGION" > /dev/null
-
-log "waiting for snapshot to become available..."
-snap_ok=0
-for _ in $(seq 1 120); do # ~20 min
-    state=$(aws lightsail get-instance-snapshot --instance-snapshot-name "$SNAPSHOT_NAME" \
-        --region "$REGION" --query 'instanceSnapshot.state' --output text 2> /dev/null || echo "")
-    [ "$state" = "available" ] && {
-        snap_ok=1
-        break
-    }
-    sleep 10
-done
-[ "$snap_ok" -eq 1 ] || die "snapshot did not become available"
-
-log "DONE."
-echo
-echo "================================================================"
-echo " Lightsail snapshot ready: ${SNAPSHOT_NAME}  (region ${REGION})"
-echo "================================================================"
-echo " Launch an instance from it:"
-echo "   aws lightsail create-instances-from-snapshot \\"
-echo "     --instance-snapshot-name ${SNAPSHOT_NAME} \\"
-echo "     --instance-names my-3xui-1 --bundle-id ${BUNDLE} \\"
-echo "     --availability-zone ${AZ} --region ${REGION}"
-if [ -n "$PANEL_PORT" ]; then
-    echo
-    echo " Then open the panel port (pinned to ${PANEL_PORT}):"
-    echo "   aws lightsail open-instance-public-ports --region ${REGION} \\"
-    echo "     --instance-name my-3xui-1 \\"
-    echo "     --port-info fromPort=${PANEL_PORT},toPort=${PANEL_PORT},protocol=TCP"
-else
-    echo
-    echo " Each instance picks a RANDOM panel port. After it boots, read it from"
-    echo "   sudo cat /etc/x-ui/credentials.txt"
-    echo " and open that TCP port in the instance's Lightsail IPv4 firewall."
-fi
-echo "================================================================"

+ 0 - 51
deploy/lightsail/launch-script.sh

@@ -1,51 +0,0 @@
-#!/bin/bash
-#
-# Amazon Lightsail launch script for 3x-ui (self-service, per-instance creds).
-#
-# Use it one of two ways when creating an Ubuntu 24.04 Lightsail instance:
-#   * Console: "Add launch script" -> paste this file.
-#   * CLI:     aws lightsail create-instances --user-data file://launch-script.sh ...
-#
-# It installs the latest 3x-ui release non-interactively and generates unique
-# random credentials for THIS instance. The full credentials land in
-# /etc/x-ui/install-result.env (mode 600); /etc/motd shows only the URL + username.
-#
-# IMPORTANT (Lightsail firewall): Lightsail only opens 22/80/443 by default. The
-# panel listens on a random high port, so after boot read the port from
-# /etc/x-ui/install-result.env and open it under the instance's Networking tab
-# (IPv4 Firewall), or pin a known port below and pre-open it.
-set -e
-export DEBIAN_FRONTEND=noninteractive
-
-# --- Non-interactive install knobs ------------------------------------------
-export XUI_NONINTERACTIVE=1
-export XUI_SSL_MODE="${XUI_SSL_MODE:-none}"
-# Pin a known panel port so you can pre-open it in the Lightsail firewall
-# (otherwise a random high port is chosen). Username/password stay random:
-#   export XUI_PANEL_PORT="54321"
-# Other optional pins (unset => secure random):
-#   export XUI_USERNAME="admin2"
-#   export XUI_PASSWORD="change-me"
-#   export XUI_WEB_BASE_PATH="panel"
-# Domain TLS instead of plain HTTP:
-#   export XUI_SSL_MODE="domain" XUI_DOMAIN="panel.example.com" XUI_ACME_EMAIL="[email protected]"
-# ----------------------------------------------------------------------------
-
-curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh | bash
-
-# /etc/motd is world-readable, so it gets ONLY non-secret info (URL + username);
-# the full credentials stay in the root-only /etc/x-ui/install-result.env
-# (mode 600) — read them with `sudo cat` over SSH.
-if [ -r /etc/x-ui/install-result.env ]; then
-    # shellcheck disable=SC1091
-    . /etc/x-ui/install-result.env
-    {
-        echo
-        echo "=== 3x-ui panel (generated on first boot) ==="
-        echo "URL:      ${XUI_ACCESS_URL:-unknown}"
-        echo "Username: ${XUI_USERNAME:-unknown}"
-        echo "Password + API token: sudo cat /etc/x-ui/install-result.env"
-        echo "Open the panel port in the Lightsail IPv4 firewall, then log in."
-        echo "============================================="
-    } >> /etc/motd 2>/dev/null || true
-fi

+ 0 - 59
deploy/lightsail/snapshot-userdata.sh

@@ -1,59 +0,0 @@
-#!/bin/bash
-#
-# Lightsail snapshot provisioning user-data (used by build-snapshot.sh).
-#
-# Installs the 3x-ui panel into a build instance but creates NO database and
-# NO credentials, and enables the first-boot unit. The instance is then snapshot
-# so that every instance launched from the snapshot generates its own unique
-# credentials on first boot (see deploy/firstboot/).
-#
-# This is the Lightsail equivalent of deploy/packer/scripts/provision.sh. It is
-# NOT for end users — use deploy/lightsail/launch-script.sh for a direct install.
-set -e
-export DEBIAN_FRONTEND=noninteractive
-
-REPO=MHSanaei/3x-ui
-XUI_DIR=/usr/local/x-ui
-RAW="https://raw.githubusercontent.com/${REPO}/main"
-
-apt-get update
-apt-get install -y --no-install-recommends \
-    ca-certificates curl tar tzdata socat openssl cron jq
-
-ARCH=$(dpkg --print-architecture) # amd64 | arm64
-VER=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name)
-if [ -z "$VER" ] || [ "$VER" = "null" ]; then
-    echo "failed to resolve 3x-ui version" >&2
-    exit 1
-fi
-
-tmp=$(mktemp -d)
-curl -fL4 --retry 3 -o "${tmp}/x.tar.gz" \
-    "https://github.com/${REPO}/releases/download/${VER}/x-ui-linux-${ARCH}.tar.gz"
-
-systemctl stop x-ui > /dev/null 2>&1 || true
-rm -rf "$XUI_DIR"
-tar -xzf "${tmp}/x.tar.gz" -C /usr/local/
-chmod +x "${XUI_DIR}/x-ui" "${XUI_DIR}/x-ui.sh"
-chmod +x "${XUI_DIR}"/bin/* 2> /dev/null || true
-cp -f "${XUI_DIR}/x-ui.sh" /usr/bin/x-ui
-chmod +x /usr/bin/x-ui
-mkdir -p /var/log/x-ui
-
-# Panel + first-boot systemd units.
-install -m 644 "${XUI_DIR}/x-ui.service.debian" /etc/systemd/system/x-ui.service
-curl -fL4 -o "${XUI_DIR}/x-ui-firstboot.sh" "${RAW}/deploy/firstboot/x-ui-firstboot.sh"
-curl -fL4 -o /etc/systemd/system/x-ui-firstboot.service "${RAW}/deploy/firstboot/x-ui-firstboot.service"
-chmod 755 "${XUI_DIR}/x-ui-firstboot.sh"
-chmod 644 /etc/systemd/system/x-ui-firstboot.service
-
-systemctl daemon-reload
-systemctl enable x-ui-firstboot.service
-systemctl enable x-ui.service
-
-# No DB, no creds in the image — first boot generates them per-instance.
-rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* /etc/x-ui/.firstboot-done 2> /dev/null || true
-
-# Marker that build-snapshot.sh polls for over SSH.
-touch /var/lib/3xui-provision-done
-echo "[snapshot-userdata] provisioned 3x-ui ${VER} (${ARCH}); no DB created."

+ 0 - 92
deploy/marketplace/aws/README.md

@@ -1,92 +0,0 @@
-# Publishing 3x-ui to the AWS Marketplace (AMI)
-
-This is the checklist for turning the Packer-built AMI into an AWS Marketplace
-listing. It assumes you have already built an AMI with
-[`../../packer/`](../../packer/) (locally or via `.github/workflows/image.yml`).
-
-> Do **not** commit AMI IDs, AWS account numbers, or credentials. The AMI ID is
-> printed to the workflow job summary at build time.
-
-## 1. Seller registration (one-time)
-
-1. Sign in to the [AWS Marketplace Management Portal](https://aws.amazon.com/marketplace/management/)
-   with the AWS account that will own the listing.
-2. Complete **seller registration** (legal entity, bank, tax interview). Required
-   before any product can be submitted.
-
-## 2. Build a compliant AMI
-
-Build in the seller account (or share the AMI into it):
-
-```bash
-cd deploy/packer
-packer init .
-# amd64
-packer build -only='amazon-ebs.x-ui' \
-  -var 'xui_version=vX.Y.Z' -var 'xui_arch=amd64' -var 'instance_type=t3.small' -var 'region=eu-central-1' .
-# arm64 (Graviton)
-packer build -only='amazon-ebs.x-ui' \
-  -var 'xui_version=vX.Y.Z' -var 'xui_arch=arm64' -var 'instance_type=t4g.small' -var 'region=eu-central-1' .
-```
-
-You can list both AMIs (amd64 + arm64) as architectures of a single Marketplace
-product, or as separate products.
-
-The image already satisfies the Marketplace AMI policies enforced by `harden.sh`
-+ `cleanup.sh`:
-
-- ✅ `PasswordAuthentication no`, `PermitRootLogin prohibit-password`
-- ✅ no default OS account passwords (all locked)
-- ✅ no baked `authorized_keys`, no SSH host keys (regenerated on boot)
-- ✅ base OS = current Ubuntu 24.04 LTS, patched at build time
-- ✅ no application default credentials — the panel admin is generated on first
-  boot on a random high port (no `admin/admin`, no shipped `x-ui.db`)
-
-## 3. Run the self-service AMI scan
-
-1. In the Management Portal: **Server products → AMIs → Upload/scan an AMI**.
-2. Share the AMI with the AWS Marketplace scanning account when prompted
-   (the portal gives you the exact account id and the `modify-image-attribute`
-   command, or share it from the EC2 console).
-3. Start the scan. It checks SSH config, default credentials, open ports, and
-   for malware. Fix any finding and re-scan.
-
-Common scan findings and where they're handled:
-
-| Finding | Fix (already in the build) |
-| --- | --- |
-| Password authentication enabled | `harden.sh` sshd drop-in |
-| Root login with password | `harden.sh` `PermitRootLogin prohibit-password` |
-| Default user password set | `harden.sh` `passwd -l` on all accounts |
-| Authorized keys present | `cleanup.sh` removes them |
-| Out-of-date packages | base image is the latest LTS; `provision.sh` runs `apt-get update` |
-
-## 4. Create the product (limited / private first)
-
-1. **Server products → Create new product → AMI** (or AMI + CloudFormation).
-2. Add title, description, categories, pricing (free or paid), regions, the AMI
-   id, recommended instance types, and the **usage instructions** (tell buyers
-   to read `/etc/x-ui/credentials.txt` / MOTD after first boot for the generated
-   admin login, then change the password).
-3. Submit as a **Limited** (private) listing first. AWS publishes it with
-   restricted visibility so only your account / allow-listed accounts see it.
-
-## 5. Preview & launch test
-
-1. From the limited listing, **subscribe and launch** a test instance.
-2. SSH in, `sudo cat /etc/x-ui/credentials.txt`, open the panel URL, log in,
-   confirm the panel works and the credentials are unique to that instance.
-3. Launch a second instance and confirm its credentials differ (no shared
-   secrets).
-
-## 6. Go public
-
-1. Once the scan passes and the preview looks correct, request **public
-   visibility** (move from Limited to Public) in the listing.
-2. AWS does a final review before the listing goes live.
-
-## References
-
-- AWS Marketplace seller guide: <https://docs.aws.amazon.com/marketplace/latest/userguide/>
-- AMI-based product requirements: <https://docs.aws.amazon.com/marketplace/latest/userguide/product-and-ami-policies.html>
-- Self-service AMI scanning: <https://docs.aws.amazon.com/marketplace/latest/userguide/product-submission.html>

+ 3 - 24
deploy/marketplace/hetzner/README.md

@@ -1,9 +1,10 @@
 # 3x-ui on Hetzner Cloud
 # 3x-ui on Hetzner Cloud
 
 
 Hetzner Cloud does **not** have a third-party image marketplace the way AWS does.
 Hetzner Cloud does **not** have a third-party image marketplace the way AWS does.
-There are two practical ways to ship 3x-ui on Hetzner.
+Ship 3x-ui via **cloud-init**: each instance installs non-interactively and
+generates unique per-instance credentials (no `admin/admin`, no shared secret).
 
 
-## Option A — cloud-init (recommended, no image build)
+## cloud-init (no image build)
 
 
 Use the generic user-data from [`../../cloud-init/`](../../cloud-init/). It installs
 Use the generic user-data from [`../../cloud-init/`](../../cloud-init/). It installs
 3x-ui non-interactively and generates unique per-instance credentials.
 3x-ui non-interactively and generates unique per-instance credentials.
@@ -27,28 +28,6 @@ After boot, fetch the generated credentials:
 ssh root@<server-ip> 'cat /etc/x-ui/install-result.env'
 ssh root@<server-ip> 'cat /etc/x-ui/install-result.env'
 ```
 ```
 
 
-## Option B — snapshot from the qcow2 / a configured server
-
-Hetzner lets you create a **snapshot** of a running server and launch new
-servers from it. Two ways to get there:
-
-1. **From the Packer qcow2:** Hetzner does not allow direct qcow2 upload via the
-   normal API, but you can boot a server, write the image to its disk in rescue
-   mode, then take a snapshot — or simply use Option A, which needs no image.
-2. **From a configured server:** spin up a server, install via cloud-init
-   (Option A), verify, then **delete `/etc/x-ui/x-ui.db` and the first-boot
-   sentinel** before snapshotting so clones regenerate their own credentials:
-
-   ```bash
-   systemctl stop x-ui
-   rm -f /etc/x-ui/x-ui.db /etc/x-ui/.firstboot-done /etc/x-ui/credentials.txt
-   # re-enable first-boot regeneration if you installed via Packer:
-   systemctl enable x-ui-firstboot 2>/dev/null || true
-   ```
-
-   > ⚠️ If you snapshot a server **with** its `x-ui.db`, every clone shares the
-   > same admin credentials and session secret. Always remove the DB first.
-
 ## "App"-style listing
 ## "App"-style listing
 
 
 Hetzner's curated apps live in the community repo
 Hetzner's curated apps live in the community repo

+ 0 - 7
deploy/packer/.gitignore

@@ -1,7 +0,0 @@
-# Packer build artifacts (never commit images or manifests)
-output-qemu/
-*.qcow2
-*.raw
-packer-manifest.json
-packer_cache/
-crash.log

+ 0 - 116
deploy/packer/README.md

@@ -1,116 +0,0 @@
-# 3x-ui golden image (Packer)
-
-Builds a cloud image with the 3x-ui panel pre-installed but **not configured**:
-the image ships with **no database and no credentials**, and generates a unique
-admin account on first boot. This is the **primary** path for AWS Marketplace
-and any reusable image.
-
-Two sources, one build:
-
-| Source | Output | For |
-| --- | --- | --- |
-| `amazon-ebs` | AWS AMI | AWS / Marketplace |
-| `qemu` | `qcow2` (+ `raw`) | Hetzner, DigitalOcean, Vultr, GCP, Azure, Oracle, bare metal |
-
-Both sources build for **`amd64` and `arm64`** (select with `-var xui_arch=...`).
-
-## Why no baked DB
-
-3x-ui seeds a hardcoded `admin/admin` user and generates its session secret +
-panel GUID the first time it starts. If an image shipped an initialized
-`x-ui.db`, **every clone would share the same credentials and secret**. So the
-build deliberately:
-
-- installs the panel binary + systemd unit but **never starts it** and **never
-  creates a DB** (`scripts/provision.sh`);
-- wipes any stray DB/credentials/host-keys at the end (`scripts/cleanup.sh`);
-- enables `x-ui-firstboot.service`, which on first boot resets settings, sets a
-  random username/password on a random high port, regenerates the secret/GUID,
-  and writes the credentials to `/etc/x-ui/credentials.txt` + `/etc/motd`
-  (`deploy/firstboot/`).
-
-## Prerequisites
-
-- [Packer](https://developer.hashicorp.com/packer) ≥ 1.9
-- For `qemu` amd64: `qemu-system-x86`, `qemu-utils` (and `/dev/kvm` for acceptable speed)
-- For `qemu` arm64: `qemu-system-arm`, `qemu-efi-aarch64`, `qemu-utils` — best built on an
-  arm64 host (native KVM); cross-building from x86 works but uses slow TCG emulation
-- For `amazon-ebs`: AWS credentials with EC2 build permissions (arm64 builds on a Graviton
-  instance such as `t4g.small`)
-
-```bash
-cd deploy/packer
-packer init .
-packer fmt -check .      # formatting
-packer validate .        # both sources
-```
-
-## Build
-
-Build a specific release (recommended) or `latest`:
-
-```bash
-# amd64 qcow2 (no cloud account needed)
-packer build -only='qemu.x-ui' -var 'xui_version=v3.3.1' -var 'xui_arch=amd64' .
-
-# arm64 qcow2 (run on an arm64 host for native KVM)
-packer build -only='qemu.x-ui' -var 'xui_version=v3.3.1' -var 'xui_arch=arm64' .
-
-# amd64 AWS AMI
-packer build -only='amazon-ebs.x-ui' \
-  -var 'xui_version=v3.3.1' -var 'xui_arch=amd64' -var 'instance_type=t3.small' -var 'region=eu-central-1' .
-
-# arm64 AWS AMI (Graviton)
-packer build -only='amazon-ebs.x-ui' \
-  -var 'xui_version=v3.3.1' -var 'xui_arch=arm64' -var 'instance_type=t4g.small' -var 'region=eu-central-1' .
-```
-
-Outputs (per arch):
-- `output-qemu/3x-ui-ubuntu-24.04-<arch>.qcow2` and `.raw`
-- the AMI id (also recorded in `packer-manifest.json`)
-
-If `/dev/kvm` is unavailable, add `-var 'qemu_accelerator=tcg'` (much slower).
-
-## Key variables
-
-See [`variables.pkr.hcl`](variables.pkr.hcl) for the full list.
-
-| Variable | Default | Notes |
-| --- | --- | --- |
-| `xui_version` | `latest` | Release tag to install, e.g. `v3.3.1` |
-| `xui_arch` | `amd64` | `amd64` or `arm64` (derives the base AMI / cloud image) |
-| `region` | `eu-central-1` | AWS region (amazon-ebs) |
-| `instance_type` | `t3.small` | EC2 build instance — must match the arch (`t4g.small` for arm64) |
-| `qemu_accelerator` | `kvm` | `kvm` or `tcg` |
-| `qemu_cpu` | `host` | arm64 `-cpu` model (`host` with KVM, `max` for TCG) |
-| `ubuntu_version` | `24.04` | Base Ubuntu LTS (naming/tags) |
-
-The CI workflow builds both arches automatically: amd64 qcow2 on a standard runner,
-arm64 qcow2 on a native `ubuntu-24.04-arm` runner, and both AMIs from a single runner
-(the build instance runs in AWS).
-
-## First boot
-
-On the first boot of any instance launched from the image:
-
-1. `x-ui-firstboot.service` runs **before** `x-ui.service`.
-2. It generates a unique admin username/password, a random panel port, a random
-   base path, and an API token.
-3. Credentials are written to `/etc/x-ui/credentials.txt` (root-only) and shown
-   in `/etc/motd`. Retrieve them with `sudo cat /etc/x-ui/credentials.txt`.
-4. The panel then starts on the random port. `admin/admin` never exists.
-
-## CI
-
-`.github/workflows/image.yml` runs this build on `release: published` (and via
-`workflow_dispatch`), attaching the compressed `qcow2` to the release and
-building the AMI when AWS credentials are configured.
-
-## A note on host firewalls
-
-`scripts/harden.sh` intentionally does **not** enable a restrictive host
-firewall. 3x-ui opens Xray inbound ports on admin-chosen ports at runtime, which
-a host firewall would block. Use your cloud provider's security groups/firewall
-instead, and open the panel port + your inbound ports there. If you still want a
-host firewall, add `ufw` rules in `harden.sh` allowing SSH, the panel port and
-your inbound ports.

+ 0 - 59
deploy/packer/scripts/cleanup.sh

@@ -1,59 +0,0 @@
-#!/usr/bin/env bash
-#
-# cleanup.sh — strip all instance-specific state and secrets from the image.
-#
-# Runs LAST. The output image must contain no panel database, no credentials,
-# no SSH host keys, and no baked authorized_keys. Fails the build if any of
-# those survive.
-set -euo pipefail
-
-echo "[cleanup] removing panel database, credentials and first-boot sentinel..."
-rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* 2> /dev/null || true
-rm -f /etc/x-ui/install-result.env /etc/x-ui/credentials.txt 2> /dev/null || true
-rm -f /etc/x-ui/.firstboot-done 2> /dev/null || true
-
-echo "[cleanup] removing SSH host keys (regenerated on first boot)..."
-rm -f /etc/ssh/ssh_host_* 2> /dev/null || true
-
-echo "[cleanup] removing any baked authorized_keys..."
-rm -f /root/.ssh/authorized_keys 2> /dev/null || true
-find /home -maxdepth 3 -name authorized_keys -type f -delete 2> /dev/null || true
-
-echo "[cleanup] resetting machine-id..."
-truncate -s 0 /etc/machine-id 2> /dev/null || true
-rm -f /var/lib/dbus/machine-id 2> /dev/null || true
-ln -sf /etc/machine-id /var/lib/dbus/machine-id 2> /dev/null || true
-
-echo "[cleanup] resetting cloud-init so it re-runs on the real first boot..."
-cloud-init clean --logs --seed > /dev/null 2>&1 || rm -rf /var/lib/cloud/* 2> /dev/null || true
-
-echo "[cleanup] truncating logs, history and package caches..."
-find /var/log -type f -exec truncate -s 0 {} + 2> /dev/null || true
-rm -rf /var/lib/x-ui /var/log/x-ui/* 2> /dev/null || true
-apt-get clean || true
-rm -rf /var/lib/apt/lists/* 2> /dev/null || true
-rm -f /root/.bash_history 2> /dev/null || true
-find /home -maxdepth 3 -name .bash_history -type f -delete 2> /dev/null || true
-rm -rf /tmp/firstboot 2> /dev/null || true
-
-echo "[cleanup] verifying the image is clean..."
-fail=0
-for f in /etc/x-ui/x-ui.db /etc/x-ui/credentials.txt /etc/x-ui/install-result.env /etc/x-ui/.firstboot-done; do
-    if [ -e "$f" ]; then
-        echo "[cleanup] FATAL: $f is present in the image" >&2
-        fail=1
-    fi
-done
-if ls /etc/ssh/ssh_host_* > /dev/null 2>&1; then
-    echo "[cleanup] FATAL: SSH host keys present in the image" >&2
-    fail=1
-fi
-if [ -e /root/.ssh/authorized_keys ]; then
-    echo "[cleanup] FATAL: /root/.ssh/authorized_keys present in the image" >&2
-    fail=1
-fi
-if [ "$fail" -ne 0 ]; then
-    exit 1
-fi
-
-echo "[cleanup] OK — no DB, no credentials, no host keys, no authorized_keys."

+ 0 - 39
deploy/packer/scripts/harden.sh

@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-#
-# harden.sh — baseline OS hardening for AWS Marketplace AMI scanner compliance.
-#
-# Focus: the controls the scanner actually checks — key-only SSH, no root
-# password login, and no default OS account passwords. A restrictive host
-# firewall is intentionally NOT enforced by default because 3x-ui opens Xray
-# inbound ports on admin-chosen ports at runtime (see README for the rationale
-# and how to add ufw rules if you want them).
-set -euo pipefail
-export DEBIAN_FRONTEND=noninteractive
-
-echo "[harden] applying SSH hardening..."
-install -d -m 755 /etc/ssh/sshd_config.d
-cat > /etc/ssh/sshd_config.d/99-3xui-hardening.conf << 'EOF'
-# 3x-ui golden image hardening (AWS Marketplace scanner compliance)
-PasswordAuthentication no
-PermitRootLogin prohibit-password
-KbdInteractiveAuthentication no
-ChallengeResponseAuthentication no
-EOF
-chmod 644 /etc/ssh/sshd_config.d/99-3xui-hardening.conf
-
-echo "[harden] locking passwords on default OS accounts..."
-# No account may ship with a usable password. Keys are provisioned per-instance
-# by the cloud platform (EC2 metadata / cloud-init) on first boot.
-# passwd -l locks the PASSWORD only; key-based login keeps working.
-for u in root ubuntu admin; do
-    if id "$u" > /dev/null 2>&1; then
-        passwd -l "$u" > /dev/null 2>&1 || true
-    fi
-done
-
-echo "[harden] enabling automatic security updates..."
-apt-get update
-apt-get install -y --no-install-recommends unattended-upgrades
-systemctl enable unattended-upgrades > /dev/null 2>&1 || true
-
-echo "[harden] done."

+ 0 - 76
deploy/packer/scripts/provision.sh

@@ -1,76 +0,0 @@
-#!/usr/bin/env bash
-#
-# provision.sh — install the 3x-ui panel into a golden image (Packer).
-#
-# Self-contained: mirrors install.sh's download/extract logic but DELIBERATELY
-# does NOT run config_after_install and does NOT create a database. The image
-# must ship without /etc/x-ui/x-ui.db so that deploy/firstboot generates unique
-# per-instance credentials on first boot. Both x-ui.service and
-# x-ui-firstboot.service are enabled but NOT started here.
-#
-# Inputs (from Packer environment_vars):
-#   XUI_VERSION  release tag (e.g. v3.3.1) or 'latest'
-#   XUI_ARCH     amd64 (default) or arm64
-set -euo pipefail
-
-XUI_VERSION="${XUI_VERSION:-latest}"
-XUI_ARCH="${XUI_ARCH:-amd64}"
-XUI_DIR="/usr/local/x-ui"
-REPO="MHSanaei/3x-ui"
-export DEBIAN_FRONTEND=noninteractive
-
-echo "[provision] installing base packages..."
-apt-get update
-apt-get install -y --no-install-recommends \
-    ca-certificates curl tar tzdata socat openssl cron jq
-
-echo "[provision] resolving 3x-ui version..."
-if [ "$XUI_VERSION" = "latest" ]; then
-    XUI_VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | jq -r '.tag_name')
-fi
-if [ -z "$XUI_VERSION" ] || [ "$XUI_VERSION" = "null" ]; then
-    echo "[provision] ERROR: could not resolve 3x-ui release tag" >&2
-    exit 1
-fi
-echo "[provision] installing 3x-ui ${XUI_VERSION} (${XUI_ARCH})"
-
-tarball="x-ui-linux-${XUI_ARCH}.tar.gz"
-url="https://github.com/${REPO}/releases/download/${XUI_VERSION}/${tarball}"
-tmp="$(mktemp -d)"
-trap 'rm -rf "$tmp"' EXIT
-
-# Download the RELEASED binary tarball (no Go build inside the image).
-curl -fL4 --retry 3 -o "${tmp}/${tarball}" "$url"
-
-# Extract into /usr/local/ (the tarball contains an x-ui/ directory).
-systemctl stop x-ui > /dev/null 2>&1 || true
-rm -rf "$XUI_DIR"
-tar -xzf "${tmp}/${tarball}" -C /usr/local/
-chmod +x "${XUI_DIR}/x-ui" "${XUI_DIR}/x-ui.sh"
-chmod +x "${XUI_DIR}"/bin/* 2> /dev/null || true
-
-# Install the x-ui management CLI.
-if [ -f "${XUI_DIR}/x-ui.sh" ]; then
-    cp -f "${XUI_DIR}/x-ui.sh" /usr/bin/x-ui
-else
-    curl -fL4 -o /usr/bin/x-ui "https://raw.githubusercontent.com/${REPO}/main/x-ui.sh"
-fi
-chmod +x /usr/bin/x-ui
-mkdir -p /var/log/x-ui
-
-# Panel systemd unit (Ubuntu base => debian variant).
-install -m 644 "${XUI_DIR}/x-ui.service.debian" /etc/systemd/system/x-ui.service
-
-# First-boot per-instance credential unit + script (uploaded to /tmp/firstboot).
-install -m 755 /tmp/firstboot/x-ui-firstboot.sh "${XUI_DIR}/x-ui-firstboot.sh"
-install -m 644 /tmp/firstboot/x-ui-firstboot.service /etc/systemd/system/x-ui-firstboot.service
-
-systemctl daemon-reload
-# Enable (start on next boot) but do NOT start now — there is no DB yet.
-systemctl enable x-ui-firstboot.service
-systemctl enable x-ui.service
-
-# Belt-and-braces: ensure no DB / sentinel was created during provisioning.
-rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* /etc/x-ui/.firstboot-done 2> /dev/null || true
-
-echo "[provision] done — panel installed, services enabled, NO database initialized."

+ 0 - 109
deploy/packer/variables.pkr.hcl

@@ -1,109 +0,0 @@
-// Input variables for the 3x-ui golden-image build.
-// See README.md for usage. Override with -var / -var-file or env (PKR_VAR_*).
-
-variable "xui_version" {
-  type        = string
-  description = "3x-ui release tag to install, e.g. v3.3.1. 'latest' resolves the newest GitHub release at build time."
-  default     = "latest"
-}
-
-variable "xui_arch" {
-  type        = string
-  description = "CPU architecture to build for: amd64 or arm64."
-  default     = "amd64"
-  validation {
-    condition     = contains(["amd64", "arm64"], var.xui_arch)
-    error_message = "The xui_arch value must be 'amd64' or 'arm64'."
-  }
-}
-
-variable "ubuntu_version" {
-  type        = string
-  description = "Ubuntu LTS version label, used only for image naming/tags."
-  default     = "24.04"
-}
-
-// --- amazon-ebs (AMI) ---------------------------------------------------------
-
-variable "region" {
-  type        = string
-  description = "AWS region the AMI is built in."
-  default     = "eu-central-1"
-}
-
-variable "instance_type" {
-  type        = string
-  description = "EC2 instance type used to build the AMI. Must match xui_arch (e.g. t3.small for amd64, t4g.small for arm64/Graviton)."
-  default     = "t3.small"
-}
-
-variable "ami_name_prefix" {
-  type        = string
-  description = "Prefix for the produced AMI name."
-  default     = "3x-ui"
-}
-
-variable "source_ami_filter_name" {
-  type        = string
-  description = "Override for the Canonical Ubuntu base AMI name filter. Empty ⇒ derived from xui_arch (latest patched 24.04 LTS for that arch)."
-  default     = ""
-}
-
-variable "ssh_username" {
-  type        = string
-  description = "Default SSH user on the base Ubuntu cloud image."
-  default     = "ubuntu"
-}
-
-// --- qemu (qcow2 / raw) -------------------------------------------------------
-
-variable "qemu_iso_url" {
-  type        = string
-  description = "Override for the Ubuntu cloud image used as the qemu base disk. Empty ⇒ derived from xui_arch (amd64/arm64 cloud image)."
-  default     = ""
-}
-
-variable "qemu_iso_checksum" {
-  type        = string
-  description = "Checksum for the qemu base disk. 'file:<SHA256SUMS url>' auto-fetches; 'none' skips verification."
-  default     = "file:https://cloud-images.ubuntu.com/releases/24.04/release/SHA256SUMS"
-}
-
-variable "qemu_accelerator" {
-  type        = string
-  description = "QEMU accelerator: 'kvm' when /dev/kvm is available, else 'tcg' (slow software emulation)."
-  default     = "kvm"
-}
-
-variable "qemu_headless" {
-  type        = bool
-  description = "Run QEMU without a display (required on CI runners)."
-  default     = true
-}
-
-variable "qemu_build_password" {
-  type        = string
-  description = "Temporary password injected via cloud-init for Packer's build-time SSH. Locked/removed before the image is finalized."
-  default     = "packer-build-temp-pw"
-  sensitive   = true
-}
-
-# --- qemu arm64-only knobs (ignored for amd64) -------------------------------
-
-variable "qemu_cpu" {
-  type        = string
-  description = "QEMU -cpu model for arm64 builds: 'host' with KVM on an arm64 host, 'max' for TCG emulation."
-  default     = "host"
-}
-
-variable "qemu_efi_code" {
-  type        = string
-  description = "Path to the arm64 UEFI code firmware (AAVMF). Only used when xui_arch=arm64."
-  default     = "/usr/share/AAVMF/AAVMF_CODE.fd"
-}
-
-variable "qemu_efi_vars" {
-  type        = string
-  description = "Path to the arm64 UEFI vars firmware template (AAVMF). Only used when xui_arch=arm64."
-  default     = "/usr/share/AAVMF/AAVMF_VARS.fd"
-}

+ 0 - 160
deploy/packer/x-ui.pkr.hcl

@@ -1,160 +0,0 @@
-// 3x-ui golden image — one build, two sources:
-//   * amazon-ebs : produces an AWS AMI (Marketplace-scannable)
-//   * qemu       : produces a qcow2 (+ raw) for Hetzner/DO/Vultr/GCP/Azure/Oracle
-//
-// The image ships WITHOUT an initialized x-ui.db and WITHOUT any baked
-// credentials. deploy/firstboot/x-ui-firstboot.{sh,service} generates unique
-// per-instance credentials on first boot, before x-ui.service starts.
-//
-// Provisioner order is fixed: provision.sh -> harden.sh -> cleanup.sh.
-
-packer {
-  required_plugins {
-    amazon = {
-      version = ">= 1.3.0"
-      source  = "github.com/hashicorp/amazon"
-    }
-    qemu = {
-      version = ">= 1.1.0"
-      source  = "github.com/hashicorp/qemu"
-    }
-  }
-}
-
-locals {
-  build_stamp = formatdate("YYYYMMDD-hhmmss", timestamp())
-  image_name  = "${var.ami_name_prefix}-ubuntu-${var.ubuntu_version}-${var.xui_arch}"
-  is_arm      = var.xui_arch == "arm64"
-
-  # Base images are derived from xui_arch unless explicitly overridden.
-  source_ami_name = var.source_ami_filter_name != "" ? var.source_ami_filter_name : "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-${var.xui_arch}-server-*"
-  qemu_iso_url    = var.qemu_iso_url != "" ? var.qemu_iso_url : "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-${var.xui_arch}.img"
-}
-
-source "amazon-ebs" "x-ui" {
-  region        = var.region
-  instance_type = var.instance_type
-  ssh_username  = var.ssh_username
-
-  ami_name        = "${local.image_name}-${var.xui_version}-${local.build_stamp}"
-  ami_description = "3x-ui panel on Ubuntu ${var.ubuntu_version}. Per-instance credentials are generated on first boot."
-
-  source_ami_filter {
-    filters = {
-      name                = local.source_ami_name
-      root-device-type    = "ebs"
-      virtualization-type = "hvm"
-    }
-    owners      = ["099720109477"] // Canonical
-    most_recent = true
-  }
-
-  launch_block_device_mappings {
-    device_name           = "/dev/sda1"
-    volume_size           = 8
-    volume_type           = "gp3"
-    delete_on_termination = true
-  }
-
-  tags = {
-    Name       = local.image_name
-    Project    = "3x-ui"
-    XuiVersion = var.xui_version
-    BuildTool  = "packer"
-    BaseOS     = "ubuntu-${var.ubuntu_version}"
-  }
-}
-
-source "qemu" "x-ui" {
-  iso_url      = local.qemu_iso_url
-  iso_checksum = var.qemu_iso_checksum
-  disk_image   = true
-  disk_size    = "10G"
-  format       = "qcow2"
-
-  accelerator    = var.qemu_accelerator
-  headless       = var.qemu_headless
-  cpus           = 2
-  memory         = 2048
-  net_device     = "virtio-net"
-  disk_interface = "virtio"
-
-  // Arch-specific QEMU machine. amd64 uses Packer defaults (BIOS boot, x86_64);
-  // arm64 needs the aarch64 binary, the 'virt' machine and UEFI (AAVMF) firmware.
-  qemu_binary       = local.is_arm ? "qemu-system-aarch64" : null
-  machine_type      = local.is_arm ? "virt" : null
-  efi_boot          = local.is_arm
-  efi_firmware_code = local.is_arm ? var.qemu_efi_code : null
-  efi_firmware_vars = local.is_arm ? var.qemu_efi_vars : null
-  qemuargs          = local.is_arm ? [["-cpu", var.qemu_cpu]] : []
-
-  output_directory = "output-qemu"
-  vm_name          = "${local.image_name}.qcow2"
-
-  // Build-time access: a NoCloud seed sets a temporary password for the default
-  // user so Packer can SSH in. The seed is a separate CD-ROM (not part of the
-  // output disk); the password is locked by harden.sh and state wiped by cleanup.sh.
-  cd_label = "cidata"
-  cd_content = {
-    "meta-data" = ""
-    "user-data" = <<-EOT
-      #cloud-config
-      password: ${var.qemu_build_password}
-      chpasswd: { expire: false }
-      ssh_pwauth: true
-    EOT
-  }
-
-  ssh_username = var.ssh_username
-  ssh_password = var.qemu_build_password
-  ssh_timeout  = "20m"
-  boot_wait    = "45s"
-
-  shutdown_command = "sudo shutdown -P now"
-}
-
-build {
-  name    = "3x-ui"
-  sources = ["source.amazon-ebs.x-ui", "source.qemu.x-ui"]
-
-  // Upload the first-boot unit + script so provision.sh can install them.
-  provisioner "shell" {
-    inline = ["mkdir -p /tmp/firstboot"]
-  }
-  provisioner "file" {
-    source      = "${path.root}/../firstboot/x-ui-firstboot.sh"
-    destination = "/tmp/firstboot/x-ui-firstboot.sh"
-  }
-  provisioner "file" {
-    source      = "${path.root}/../firstboot/x-ui-firstboot.service"
-    destination = "/tmp/firstboot/x-ui-firstboot.service"
-  }
-
-  provisioner "shell" {
-    environment_vars = [
-      "XUI_VERSION=${var.xui_version}",
-      "XUI_ARCH=${var.xui_arch}",
-      "DEBIAN_FRONTEND=noninteractive",
-    ]
-    execute_command = "chmod +x {{ .Path }}; sudo -E bash {{ .Path }}"
-    scripts = [
-      "${path.root}/scripts/provision.sh",
-      "${path.root}/scripts/harden.sh",
-      "${path.root}/scripts/cleanup.sh",
-    ]
-    // give cloud-init time to release apt locks on the very first boot
-    pause_before = "10s"
-  }
-
-  // Convert the qcow2 to raw for clouds that need it (qemu source only).
-  post-processor "shell-local" {
-    only   = ["qemu.x-ui"]
-    inline = ["qemu-img convert -p -O raw output-qemu/${local.image_name}.qcow2 output-qemu/${local.image_name}.raw"]
-  }
-
-  // Record the AMI id / artifacts for CI to surface.
-  post-processor "manifest" {
-    output     = "packer-manifest.json"
-    strip_path = true
-  }
-}

+ 0 - 86
deploy/test/smoke-firstboot.sh

@@ -1,86 +0,0 @@
-#!/usr/bin/env bash
-#
-# smoke-firstboot.sh — verify the first-boot per-instance credential script.
-#
-# Installs the released x-ui binary into a container WITHOUT a database, runs
-# x-ui-firstboot.sh, and asserts:
-#   * fresh random credentials are generated (no admin/admin)
-#   * /etc/x-ui/credentials.txt (600) and /etc/motd are written
-#   * the sentinel is created and a second run is a no-op (creds unchanged)
-#
-# Requires Docker and network access. Usage: bash deploy/test/smoke-firstboot.sh
-set -euo pipefail
-
-REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-IMAGE="${SMOKE_IMAGE:-ubuntu:24.04}"
-
-if ! command -v docker > /dev/null 2>&1; then
-    echo "ERROR: docker is required for this smoke test." >&2
-    exit 1
-fi
-
-echo "== first-boot credential smoke test (image: $IMAGE) =="
-
-docker run --rm \
-    -v "${REPO_ROOT}/deploy/firstboot/x-ui-firstboot.sh:/root/x-ui-firstboot.sh:ro" \
-    -e DEBIAN_FRONTEND=noninteractive \
-    "$IMAGE" bash -euo pipefail -c '
-        apt-get update -qq
-        apt-get install -y -qq curl tar openssl ca-certificates jq > /dev/null
-
-        echo "--- installing released x-ui binary (no DB, no systemd) ---"
-        REPO=MHSanaei/3x-ui
-        ARCH=$(dpkg --print-architecture)   # amd64 | arm64
-        echo "container arch: $ARCH"
-        VER=$(curl --fail --location --silent --show-error \
-            --retry 5 --retry-all-errors --retry-delay 3 \
-            --connect-timeout 15 --max-time 60 \
-            "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name)
-        [ -n "$VER" ] && [ "$VER" != "null" ] || { echo "FAIL: cannot resolve version"; exit 1; }
-        tmp=$(mktemp -d)
-        # 504s and other transient GitHub/CDN hiccups are retried; a real HTTP
-        # failure (e.g. missing arch asset) still aborts after the retries.
-        if ! curl -4 --fail --location --silent --show-error \
-            --retry 5 --retry-all-errors --retry-delay 3 \
-            --connect-timeout 15 --max-time 300 \
-            -o "${tmp}/x.tar.gz" \
-            "https://github.com/${REPO}/releases/download/${VER}/x-ui-linux-${ARCH}.tar.gz"; then
-            echo "FAIL: cannot download x-ui-linux-${ARCH}.tar.gz (${VER})" >&2; exit 1
-        fi
-        test -s "${tmp}/x.tar.gz" || { echo "FAIL: downloaded tarball is empty"; exit 1; }
-        tar -xzf "${tmp}/x.tar.gz" -C /usr/local/
-        chmod +x /usr/local/x-ui/x-ui
-        install -m 755 /root/x-ui-firstboot.sh /usr/local/x-ui/x-ui-firstboot.sh
-
-        # Guarantee a clean slate (the image must never ship a DB).
-        rm -f /etc/x-ui/x-ui.db /etc/x-ui/.firstboot-done
-
-        echo "--- run 1: generate per-instance credentials ---"
-        /usr/local/x-ui/x-ui-firstboot.sh
-
-        test -f /etc/x-ui/.firstboot-done || { echo "FAIL: sentinel not created"; exit 1; }
-        test -f /etc/x-ui/credentials.txt || { echo "FAIL: credentials.txt missing"; exit 1; }
-        perms=$(stat -c %a /etc/x-ui/credentials.txt)
-        [ "$perms" = "600" ] || { echo "FAIL: credentials.txt perms=$perms (want 600)"; exit 1; }
-        grep -q "3x-ui" /etc/motd || { echo "FAIL: motd not written"; exit 1; }
-
-        # shellcheck disable=SC1090
-        . /etc/x-ui/credentials.txt
-        [ -n "${XUI_USERNAME:-}" ] && [ "$XUI_USERNAME" != "admin" ] \
-            || { echo "FAIL: username missing or still admin"; exit 1; }
-        first_user="$XUI_USERNAME"
-
-        /usr/local/x-ui/x-ui setting -show | grep -q "hasDefaultCredential: false" \
-            || { echo "FAIL: hasDefaultCredential is not false"; exit 1; }
-
-        echo "--- run 2: must be a no-op (sentinel honored) ---"
-        /usr/local/x-ui/x-ui-firstboot.sh
-        # shellcheck disable=SC1090
-        . /etc/x-ui/credentials.txt
-        [ "$XUI_USERNAME" = "$first_user" ] \
-            || { echo "FAIL: credentials changed on re-run"; exit 1; }
-
-        echo "SMOKE_PASS: firstboot user=$first_user (stable across re-run)"
-    '
-
-echo "== first-boot smoke test PASSED =="