golang

Как запустить публичный сайт на телефоне или экономим на спичках

  • пятница, 7 февраля 2025 г. в 00:00:15
https://habr.com/ru/articles/879818/

Сейчас вас научу "плохому" — будем поднимать наше веб-приложение на телефоне и прикрутим к нему публичный домен, что бы все могли пользоваться нашим классным сервисом.

Для этой цели я накидал приложение на go, которое определяет IP адрес, вычисляет город, отправляет запрос во внешний сервис и отдает страницу с данными о погоде в вашей локации. Я не стал упарываться - он просто нужен для демонстрации. https://github.com/itcaat/what-is-the-weather-now. Также там есть кеширование погоды и городов, но скорее всего хабраэффект его положит.

Дисклеймер

В статье мы просто развлекаемся и вообще это не продакшен-реди решение ;) Но я почти уверен, что для себя вы найдете то, что сможете использовать в работе или пет-проекте.

image.png
Наша погода

Что нам нужно:

  1. Само веб-приложение.

  2. Установленный UserLAnd https://userland.tech/ ( root не потребуется )

  3. GitHub Actions чтобы собрать приложение.

  4. Бесплатный аккаунт на CloudFlare с нашим подключенным доменом — у меня будет devopsbrain.ru.

Итак, качаем UserLAnd можно с play market. В списке операционных систем выбираем Ubuntu (Minimal → Terminal) и сразу там сделаем sudo passwd userland чтобы установить пароль пользователя userland. У меня не пускало удаленно по ssh если просто passwd сделать. Я не стал разбираться, вроде на github есть какое то issue на эту тему. Поэтому едем дальше.

Теперь посмотрим в настройках wifi свой IP-адрес и подключимся с компа по ssh (порт 2022) и сразу установим пакетики.

ssh userland@192.168.1.75 -p2022
sudo apt update && apt install ca-certificates nano jq unzip -y

Само приложение и его сборка у меня уже готовы. Я собираю сразу под все платформы и архитектуры и качу релиз из main бранчи. Хотя нам нужен только arm64 для нашего эксперимента.

.github/workflows/release.yml
name: Build, Tag, and Release

on:
  push:
    branches:
      - main

env:
  appName: what-it-the-weather-now

jobs:
  version:
    outputs:
      app_version: ${{ steps.version.outputs.new_tag }}
      changelog: ${{ steps.version.outputs.changelog }}
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Bump version and push tag
        id: version
        uses: mathieudutour/github-tag-action@v6.2
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        goos: [linux, windows, darwin]
        goarch: [amd64, arm64]

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.23'

    - name: Set environment variables
      run: |
        echo "GOOS=${{ matrix.goos }}" >> $GITHUB_ENV
        echo "GOARCH=${{ matrix.goarch }}" >> $GITHUB_ENV

    - name: Install dependencies
      run: go mod download

    - name: Build
      run: |
        go build -o ${{ env.appName }}-${{ matrix.goos }}-${{ matrix.goarch }}

    - name: Package binary into a ZIP file
      run: |
        zip -j ${{ env.appName }}-${{ matrix.goos }}-${{ matrix.goarch }}.zip ${{ env.appName }}-${{ matrix.goos }}-${{ matrix.goarch }}

    - name: List generated files
      run: ls -lh ${{ env.appName }}-*.zip

    - name: Upload ZIP artifact
      uses: actions/upload-artifact@v4
      with:
        name: ${{ env.appName }}-${{ matrix.goos }}-${{ matrix.goarch }}
        path: ${{ env.appName }}-${{ matrix.goos }}-${{ matrix.goarch }}.zip

  release:
    needs: 
      - version
      - build
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Download all artifacts
      uses: actions/download-artifact@v4
      with:
        pattern: ${{ env.appName }}-*
        path: my-artifact
        merge-multiple: true

    - name: Create GitHub Release
      id: create_release
      uses: ncipollo/release-action@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag: ${{ needs.version.outputs.app_version }}
        name: Release ${{ needs.version.outputs.app_version }}
        body: ${{ needs.version.outputs.changelog }}
        generateReleaseNotes: true

    - name: List downloaded files
      run: ls -lh my-artifact

    - name: Upload all release artifacts
      run: |
        for file in ./my-artifact/*.zip; do
          echo "Uploading $file"
          gh release upload ${{ needs.version.outputs.app_version }} "$file"
        done
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Деплоить на телефон мы будем максимально просто - сделаем скрипт, который будет находить последний релиз и разворачивать в userland.

/deploy/install_and_run.sh
#!/bin/bash

REPO="itcaat/what-it-the-weather-now"
INSTALL_DIR="$HOME/what-it-the-weather-now"
BIN_NAME="what-it-the-weather-now-linux-arm64"
ZIP_FILE="$BIN_NAME.zip"
PID_FILE="$INSTALL_DIR/app.pid"
VERSION_FILE="$INSTALL_DIR/version"

FORCE_UPDATE=false

# Check if --force is used
if [[ "$1" == "--force" ]]; then
    FORCE_UPDATE=true
    echo "Force update enabled. Killing all instances and reinstalling..."
fi

# Function to stop the application
stop_application() {
    if [[ -f "$PID_FILE" ]]; then
        APP_PID=$(cat "$PID_FILE")
        if ps -p "$APP_PID" > /dev/null 2>&1; then
            echo "Stopping running application (PID: $APP_PID)..."
            kill "$APP_PID"
            sleep 2
        fi
        rm -f "$PID_FILE"
    fi
}

# Force stop all instances if --force is enabled
if [[ "$FORCE_UPDATE" == true ]]; then
    pkill -f "$BIN_NAME" 2>/dev/null
    echo "Killed all running instances of $BIN_NAME."
    rm -f "$PID_FILE" "$VERSION_FILE"
fi

# 1. Get the latest release tag from GitHub API
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name')
if [[ -z "$LATEST_RELEASE" || "$LATEST_RELEASE" == "null" ]]; then
    echo "Error: Failed to fetch the latest release."
    exit 1
fi

echo "Latest release: $LATEST_RELEASE"

# 2. Check if the installed version is already the latest
if [[ -f "$VERSION_FILE" && "$FORCE_UPDATE" == false ]]; then
    INSTALLED_VERSION=$(cat "$VERSION_FILE")
    if [[ "$INSTALLED_VERSION" == "$LATEST_RELEASE" ]]; then
        echo "You already have the latest version ($INSTALLED_VERSION). Exiting."
        exit 0
    fi
fi

# 3. Stop the running application if needed
stop_application

# 4. Construct the download URL
ASSET_URL="https://github.com/$REPO/releases/download/$LATEST_RELEASE/$ZIP_FILE"

echo "Downloading: $ASSET_URL"

# 5. Create the installation directory if it doesn't exist
mkdir -p "$INSTALL_DIR"

# 6. Download and extract the new version
curl -L "$ASSET_URL" -o "$INSTALL_DIR/$ZIP_FILE"
if [[ $? -ne 0 ]]; then
    echo "Error: Failed to download the file."
    exit 1
fi

echo "Extracting to $INSTALL_DIR"
unzip -o "$INSTALL_DIR/$ZIP_FILE" -d "$INSTALL_DIR"
chmod +x "$INSTALL_DIR/$BIN_NAME"

# 7. Remove the ZIP file after extraction
rm "$INSTALL_DIR/$ZIP_FILE"

# 8. Store the new version number
echo "$LATEST_RELEASE" > "$VERSION_FILE"

# 9. Start the updated application in the background
echo "Starting the updated application..."
nohup "$INSTALL_DIR/$BIN_NAME" > "$INSTALL_DIR/output.log" 2>&1 &
echo $! > "$PID_FILE"

echo "Application is running in the background (PID: $(cat $PID_FILE)). Logs: $INSTALL_DIR/output.log"

Можно передать параметр --force, который убьет все процессы нашего приложения и заново скачает и запустит все. Также если скрипт обнаружит новый релиз, то также стопнет текущие процессы нашего приложения и раскатит новую версию.

Почему не github actions для деплоя

Пробовал - не хватило памяти запуститься раннеру

Теперь просто кладем его в домашний каталог и запускаем. Он найдет последний релиз, скачает его под нашу платформу arm64 и запустит в фоне приложение. Оно у нас вешается на порт 8080.

image.png
image.png

Дальше остается просто добавить туннель в cloudflare zerotrust. При активации вас попросит вбить карту - можно скипнуть этот шаг и сразу настроить туннель cloudflared.

По сути нам надо просто выделить либо корневой домен, либо какой то поддомен. Мы сделаем для https://weather.devopsbrain.ru. Обслуживание домена у вас должно быть в cloudflare.

image.png
Создание нового туннеля

Далее нам нужно выбрать нужную архитектуру и операционную систему. В нашем случае debian arm64 и запустить команду для установки cloudflared.

image.png
Создание нового туннеля

После установки зароутим трафик в туннель на localhost:8080.

image.png
Создание нового туннеля

По итогу туннель будет запущен и можно открывать наш супер сайт https://weather.devopsbrain.ru, который хостится прямо на нашем телефоне. SSL также будет из коробки выдан cloudflare. При желании можете в rules настроить редиректы http -> https.

image.png
Наша погода

Ну и как вы понимаете, запустить в принципе можно все что хотите (даже с бд-шками) при достаточном количестве памяти. Исходники приложения, скрипты и файлы для сборки лежат в репе https://github.com/itcaat/what-is-the-weather-now.

А на этом все - спасибо за внимание. При желании заглядывайте в тележку https://t.me/devopsbrain. Всем отличного настроения.