From 070ab2650128b6af03d72be29f90f78cbf3584bc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 12 May 2026 03:23:50 +0900 Subject: [PATCH] refac: reorganize scripts and ci workflows --- .github/ISSUE_TEMPLATE/feature_request.yaml | 66 +- .github/dependabot.yml | 15 +- .github/pull_request_template.md | 67 +- .github/workflows/backend.yaml | 40 + .github/workflows/build-release.yml | 61 -- .github/workflows/docker-build.yaml | 917 ------------------- .github/workflows/docker.yaml | 311 +++++++ .github/workflows/format-backend.yaml | 46 - .github/workflows/format-build-frontend.yaml | 65 -- .github/workflows/frontend.yaml | 63 ++ .github/workflows/release.yml | 69 ++ backend/start.sh | 111 ++- confirm_remove.sh | 13 - docker-cleanup.sh | 16 + docker-compose-launcher.sh | 175 ++++ docker-ollama.sh | 37 + docker-run.sh | 32 + docker-update-models.sh | 24 + run-compose.sh | 250 ----- run-ollama-docker.sh | 19 - run.sh | 19 - update_ollama_models.sh | 10 - 22 files changed, 903 insertions(+), 1523 deletions(-) create mode 100644 .github/workflows/backend.yaml delete mode 100644 .github/workflows/build-release.yml delete mode 100644 .github/workflows/docker-build.yaml create mode 100644 .github/workflows/docker.yaml delete mode 100644 .github/workflows/format-backend.yaml delete mode 100644 .github/workflows/format-build-frontend.yaml create mode 100644 .github/workflows/frontend.yaml create mode 100644 .github/workflows/release.yml delete mode 100755 confirm_remove.sh create mode 100755 docker-cleanup.sh create mode 100755 docker-compose-launcher.sh create mode 100755 docker-ollama.sh create mode 100755 docker-run.sh create mode 100755 docker-update-models.sh delete mode 100755 run-compose.sh delete mode 100644 run-ollama-docker.sh delete mode 100644 run.sh delete mode 100755 update_ollama_models.sh diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 05dc6cfa94..d5f3a86132 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,82 +1,76 @@ name: Feature Request -description: Suggest an idea for this project +description: Suggest a new feature or improvement title: 'feat: ' labels: ['triage'] body: - type: markdown attributes: value: | - ## Important Notes - ### Before submitting - - Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted. - It's likely we're already tracking it! If you’re unsure, start a discussion post first. + ## Before Submitting - #### Scope + Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead. - If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead. - **We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.** - This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers. - Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions). - - If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it. - - This will help us efficiently focus on improving the project. - - ### Collaborate respectfully - We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**. + ### Scope Guidelines - Remember: - - Open WebUI is a **volunteer-driven project** - - It's managed by a **single maintainer** - - It's supported by contributors who also have **full-time jobs** + Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items. - We appreciate your time and ask that you **respect ours**. + If your request might impact the broader community, please open a Discussion first so others can weigh in on the design. + + ### Be Respectful + + Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy. ### Contributing - If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI. - ### Bug reproducibility - If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help! + If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality. + + ### Reproducibility + + If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance. - type: checkboxes id: existing-issue attributes: label: Check Existing Issues - description: Please confirm that you've checked for existing similar requests + description: Confirm you have searched for similar requests. options: - - label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request. + - label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request. required: true + - type: checkboxes id: feature-scope attributes: label: Verify Feature Scope - description: Please confirm the feature's scope is within the described scope + description: Confirm this request belongs in Issues rather than Discussions. options: - - label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions. + - label: I believe this feature request is appropriately scoped for the Issues section as described above. required: true + - type: textarea id: problem-description attributes: label: Problem Description - description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is. - placeholder: "Ex. I'm always frustrated when... / Not related to a problem" + description: Is this related to a problem? Describe the pain point clearly. + placeholder: "e.g., I'm frustrated when..." validations: required: true + - type: textarea id: solution-description attributes: - label: Desired Solution you'd like - description: Clearly describe what you want to happen. + label: Proposed Solution + description: Describe what you would like to happen. validations: required: true + - type: textarea id: alternatives-considered attributes: label: Alternatives Considered - description: A clear and concise description of any alternative solutions or features you've considered. + description: Describe any alternative solutions or workarounds you have considered. + - type: textarea id: additional-context attributes: label: Additional Context - description: Add any other context or screenshots about the feature request here. + description: Add any other context, mockups, or screenshots about the feature request. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1c83fd305b..5998b3dc96 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,17 +4,22 @@ updates: directory: '/' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - package-ecosystem: pip directory: '/backend' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - - package-ecosystem: 'github-actions' + - package-ecosystem: github-actions directory: '/' schedule: - # Check for updates to GitHub Actions every week interval: monthly - target-branch: 'dev' + target-branch: dev + + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + target-branch: dev diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2f44750fe0..957f5a3768 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,75 +19,74 @@ The most impactful way to contribute to Open WebUI is through well-written bug r **Before submitting, make sure you've checked the following:** - [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review. -- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** -- [ ] **Description:** Provide a concise description of the changes made in this pull request down below. -- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. -- [ ] **Documentation:** Add docs in [Open WebUI Docs Repository](https://github.com/open-webui/docs). Document user-facing behavior, environment variables, public APIs/interfaces, or deployment steps. -- [ ] **Dependencies:** Are there any new or upgraded dependencies? If so, explain why, update the changelog/docs, and include any compatibility notes. Actually run the code/function that uses updated library to ensure it doesn't crash. -- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Include reproducible steps to demonstrate the issue before the fix. Test edge cases (URL encoding, HTML entities, types). Take this as an opportunity to **make screenshots of the feature/fix and include them in the PR description**. -- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR. -- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? -- [ ] **Design & Architecture:** Prefer smart defaults over adding new settings; use local state for ephemeral UI logic. Open a Discussion for major architectural or UX changes. -- [ ] **Git Hygiene:** Keep PRs atomic (one logical change). Clean up commits and rebase on `dev` to ensure no unrelated commits (e.g. from `main`) are included. Push updates to the existing PR branch instead of closing and reopening. -- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following: - - **BREAKING CHANGE**: Significant changes that may affect compatibility - - **build**: Changes that affect the build system or external dependencies - - **ci**: Changes to our continuous integration processes or workflows - - **chore**: Refactor, cleanup, or other non-functional code changes - - **docs**: Documentation update or addition - - **feat**: Introduces a new feature or enhancement to the codebase - - **fix**: Bug fix or error correction +- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** +- [ ] **Description:** A concise description of the changes is provided below. +- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom. +- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs). +- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented. +- [ ] **Testing:** Manual tests have been performed to verify the fix/feature works correctly and does not introduce regressions. Screenshots or recordings are included where applicable. +- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately. +- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards. +- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first. +- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits. +- [ ] **Title Prefix:** The PR title uses one of the following prefixes: + - **BREAKING CHANGE**: Changes affecting backward compatibility + - **build**: Build system or dependency changes + - **ci**: CI/CD workflow changes + - **chore**: Refactoring, cleanup, or non-functional changes + - **docs**: Documentation additions or updates + - **feat**: New features or enhancements + - **fix**: Bug fixes or corrections - **i18n**: Internationalization or localization changes - - **perf**: Performance improvement - - **refactor**: Code restructuring for better maintainability, readability, or scalability - - **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.) - - **test**: Adding missing tests or correcting existing tests - - **WIP**: Work in progress, a temporary label for incomplete or ongoing work + - **perf**: Performance improvements + - **refactor**: Code restructuring + - **style**: Formatting changes (whitespace, semicolons, etc.) + - **test**: Test additions or corrections + - **WIP**: Work in progress # Changelog Entry ### Description -- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] +- [Describe the changes, including motivation and impact] ### Added -- [List any new features, functionalities, or additions] +- [New features, functionalities, or additions] ### Changed -- [List any changes, updates, refactorings, or optimizations] +- [Changes, updates, refactorings, or optimizations] ### Deprecated -- [List any deprecated functionality or features that have been removed] +- [Deprecated functionality or features] ### Removed -- [List any removed features, files, or functionalities] +- [Removed features, files, or functionalities] ### Fixed -- [List any fixes, corrections, or bug fixes] +- [Bug fixes or corrections] ### Security -- [List any new or updated security-related changes, including vulnerability fixes] +- [Security-related changes or vulnerability fixes] ### Breaking Changes -- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality] +- **BREAKING CHANGE**: [Changes affecting compatibility or functionality] --- ### Additional Information -- [Insert any additional context, notes, or explanations for the changes] - - [Reference any related issues, commits, or other relevant information] +- [Any additional context, notes, or references to related issues/commits] ### Screenshots or Videos -- [Attach any relevant screenshots or videos demonstrating the changes] +- [Attach relevant screenshots or videos demonstrating the changes] ### Contributor License Agreement diff --git a/.github/workflows/backend.yaml b/.github/workflows/backend.yaml new file mode 100644 index 0000000000..877a6b0ecc --- /dev/null +++ b/.github/workflows/backend.yaml @@ -0,0 +1,40 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Backend CI — Python formatting checks via Ruff +# Runs on pushes and PRs to main/dev when backend files change +# ───────────────────────────────────────────────────────────────────────────── +name: Python CI + +on: + push: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Ruff format check across supported Python versions ─────────────────── + format-check: + name: Ruff Format (${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install formatter + run: pip install "ruff>=0.15.5" + + - name: Verify formatting + run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 2ecb013e0b..0000000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release - -on: - push: - branches: - - main # or whatever branch you want to use - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Check for changes in package.json - run: | - git diff --cached --diff-filter=d package.json || { - echo "No changes to package.json" - exit 1 - } - - - name: Get version number from package.json - id: get_version - run: | - VERSION=$(jq -r '.version' package.json) - echo "::set-output name=version::$VERSION" - - - name: Extract latest CHANGELOG entry - run: | - VERSION="${{ steps.get_version.outputs.version }}" - awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md > /tmp/release-notes.md - - - name: Create GitHub release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "v${{ steps.get_version.outputs.version }}" \ - --title "v${{ steps.get_version.outputs.version }}" \ - --notes-file /tmp/release-notes.md - - - name: Upload package to GitHub release - uses: actions/upload-artifact@v4 - with: - name: package - path: | - . - !.git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Trigger Docker build workflow - uses: actions/github-script@v8 - with: - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'docker-build.yaml', - ref: 'v${{ steps.get_version.outputs.version }}', - }) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml deleted file mode 100644 index 0307593476..0000000000 --- a/.github/workflows/docker-build.yaml +++ /dev/null @@ -1,917 +0,0 @@ -name: Create and publish Docker images with specific build args - -on: - workflow_dispatch: - push: - branches: - - main - - dev - tags: - - v* - -env: - REGISTRY: ghcr.io - -jobs: - build-main-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (latest) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-main-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda126-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda126 tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda126-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda126) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - USE_CUDA_VER=cu126 - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda126-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-ollama-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-ollama-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (ollama) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_OLLAMA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-ollama-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-slim-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-slim-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (slim) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_SLIM=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-slim-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge-main-images: - runs-on: ubuntu-latest - needs: [build-main-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-main-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda-images: - runs-on: ubuntu-latest - needs: [build-cuda-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda126-images: - runs-on: ubuntu-latest - needs: [build-cuda126-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda126-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-ollama-images: - runs-on: ubuntu-latest - needs: [build-ollama-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-ollama-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-slim-images: - runs-on: ubuntu-latest - needs: [build-slim-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-slim-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - # Copy images from GHCR to Docker Hub (best-effort, won't block GHCR) - copy-to-dockerhub: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') - needs: [merge-main-images, merge-cuda-images, merge-cuda126-images, merge-ollama-images, merge-slim-images] - continue-on-error: true - strategy: - fail-fast: false - matrix: - include: - - variant: main - suffix: "" - - variant: cuda - suffix: "-cuda" - - variant: cuda126 - suffix: "-cuda126" - - variant: ollama - suffix: "-ollama" - - variant: slim - suffix: "-slim" - steps: - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Determine source and destination tags - id: tags - run: | - DOCKERHUB_IMAGE="openwebui/open-webui" - SUFFIX="${{ matrix.suffix }}" - - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - # For version tags: copy version tag and major.minor tag - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" - MAJOR_MINOR="${VERSION%.*}" - - echo "tags<> $GITHUB_OUTPUT - echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT - echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - else - # For main branch - if [ -z "$SUFFIX" ]; then - echo "tags=latest" >> $GITHUB_OUTPUT - else - # e.g. latest-cuda -> also tag as just "cuda" - VARIANT_NAME="${SUFFIX#-}" - echo "tags<> $GITHUB_OUTPUT - echo "latest${SUFFIX}" >> $GITHUB_OUTPUT - echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - fi - fi - - echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT - - - name: Copy images from GHCR to Docker Hub - run: | - DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}" - SUFFIX="${{ matrix.suffix }}" - - # Determine the source tag on GHCR - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" - SOURCE_TAG="${VERSION}${SUFFIX}" - else - if [ -z "$SUFFIX" ]; then - SOURCE_TAG="latest" - else - SOURCE_TAG="latest${SUFFIX}" - fi - fi - - SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}" - - echo "Copying from ${SOURCE} to Docker Hub..." - - # Copy each destination tag - while IFS= read -r TAG; do - [ -z "$TAG" ] && continue - DEST="${DOCKERHUB_IMAGE}:${TAG}" - echo " -> ${DEST}" - docker buildx imagetools create -t "${DEST}" "${SOURCE}" - done <<< "${{ steps.tags.outputs.tags }}" diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000000..075a5da434 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,311 @@ +name: Create and publish Docker images with specific build args + +on: + workflow_dispatch: + push: + branches: + - main + - dev + tags: + - v* + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - arch: linux/amd64 + runner: ubuntu-latest + - arch: linux/arm64 + runner: ubuntu-24.04-arm + variant: + - name: main + suffix: "" + build_args: "" + free_disk: false + - name: cuda + suffix: "-cuda" + build_args: "USE_CUDA=true" + free_disk: true + - name: cuda126 + suffix: "-cuda126" + build_args: | + USE_CUDA=true + USE_CUDA_VER=cu126 + free_disk: true + - name: ollama + suffix: "-ollama" + build_args: "USE_OLLAMA=true" + free_disk: false + - name: slim + suffix: "-slim" + build_args: "USE_SLIM=true" + free_disk: false + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + platform=${{ matrix.platform.arch }} + echo "PLATFORM_PAIR=${platform//\//-}" >> ${GITHUB_ENV} + + - name: Free disk space + if: matrix.variant.free_disk + run: rm -rf /opt/hostedtoolcache + + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-${{ matrix.variant.name }}-${{ matrix.platform.arch }}- + latest=false + + - name: Build Docker image + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform.arch }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true + build-args: | + BUILD_HASH=${{ github.sha }} + ${{ matrix.variant.build_args }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.variant.name }}-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: [build] + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + variant: + - name: main + suffix: "" + - name: cuda + suffix: "-cuda" + - name: cuda126 + suffix: "-cuda126" + - name: ollama + suffix: "-ollama" + - name: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Download digests + uses: actions/download-artifact@v5 + with: + pattern: digests-${{ matrix.variant.name }}-* + path: /tmp/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + copy-to-dockerhub: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + needs: [merge] + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - variant: main + suffix: "" + - variant: cuda + suffix: "-cuda" + - variant: cuda126 + suffix: "-cuda126" + - variant: ollama + suffix: "-ollama" + - variant: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine source and destination tags + id: tags + run: | + DOCKERHUB_IMAGE="openwebui/open-webui" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + MAJOR_MINOR="${VERSION%.*}" + + echo "tags<> $GITHUB_OUTPUT + echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT + echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + if [ -z "$SUFFIX" ]; then + echo "tags=latest" >> $GITHUB_OUTPUT + else + VARIANT_NAME="${SUFFIX#-}" + echo "tags<> $GITHUB_OUTPUT + echo "latest${SUFFIX}" >> $GITHUB_OUTPUT + echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + fi + + echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT + + - name: Copy images from GHCR to Docker Hub + run: | + DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + SOURCE_TAG="${VERSION}${SUFFIX}" + else + if [ -z "$SUFFIX" ]; then + SOURCE_TAG="latest" + else + SOURCE_TAG="latest${SUFFIX}" + fi + fi + + SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}" + + echo "Copying from ${SOURCE} to Docker Hub..." + + while IFS= read -r TAG; do + [ -z "$TAG" ] && continue + DEST="${DOCKERHUB_IMAGE}:${TAG}" + echo " -> ${DEST}" + docker buildx imagetools create -t "${DEST}" "${SOURCE}" + done <<< "${{ steps.tags.outputs.tags }}" diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml deleted file mode 100644 index ee2d689d89..0000000000 --- a/.github/workflows/format-backend.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Python CI - -on: - push: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format Backend' - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: - - 3.11.x - - 3.12.x - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '${{ matrix.python-version }}' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install "ruff>=0.15.5" - - - name: Ruff format check - run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml deleted file mode 100644 index eaa1072fbc..0000000000 --- a/.github/workflows/format-build-frontend.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Frontend Build - -on: - push: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format & Build Frontend' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm install --force - - - name: Format Frontend - run: npm run format - - - name: Run i18next - run: npm run i18n:parse - - - name: Check for Changes After Format - run: git diff --exit-code - - - name: Build Frontend - run: npm run build - - test-frontend: - name: 'Frontend Unit Tests' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm ci --force - - - name: Run vitest - run: npm run test:frontend diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml new file mode 100644 index 0000000000..9b5f2a099f --- /dev/null +++ b/.github/workflows/frontend.yaml @@ -0,0 +1,63 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Frontend CI — Lint, format check, build, and unit tests +# Runs on pushes and PRs to main/dev, skipping backend-only changes +# ───────────────────────────────────────────────────────────────────────────── +name: Frontend Build + +on: + push: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Format, i18n, and production build ──────────────────────────────────── + format-and-build: + name: Format & Build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + run: npm install --force + + - name: Verify code formatting + run: npm run format + + - name: Verify i18n strings + run: npm run i18n:parse + + - name: Ensure working tree is clean + run: git diff --exit-code + + - name: Production build + run: npm run build + + # ── Vitest unit tests ──────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies (frozen lockfile) + run: npm ci --force + + - name: Execute test suite + run: npm run test:frontend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8654c2f7db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Release — Create GitHub release from CHANGELOG, trigger Docker builds +# Runs on pushes to main when package.json version changes +# ───────────────────────────────────────────────────────────────────────────── +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # ── Create release and trigger downstream workflows ────────────────────── + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Abort if package.json unchanged + run: | + git diff --cached --diff-filter=d package.json || { + echo "package.json not modified — skipping release" + exit 1 + } + + - name: Read version + id: pkg + run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG + run: | + VER="${{ steps.pkg.outputs.version }}" + awk "/^## \[${VER}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" \ + CHANGELOG.md > /tmp/release-notes.md + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ steps.pkg.outputs.version }}" \ + --title "v${{ steps.pkg.outputs.version }}" \ + --notes-file /tmp/release-notes.md + + - name: Archive source + uses: actions/upload-artifact@v4 + with: + name: release-archive + path: | + . + !.git + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Dispatch Docker build + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'docker.yaml', + ref: 'v${{ steps.pkg.outputs.version }}', + }) diff --git a/backend/start.sh b/backend/start.sh index 00d02f326b..fd283d31fd 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -1,86 +1,101 @@ #!/usr/bin/env bash +set -euo pipefail -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd "$SCRIPT_DIR" || exit +# --------------------------------------------------------------------------- +# Container entry point for Open WebUI. +# Handles secret key generation, optional Ollama/CUDA/Playwright setup, +# HuggingFace Space deployment, and launches the uvicorn server. +# --------------------------------------------------------------------------- + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +cd "$SCRIPT_DIR" || exit 1 + +# ── Playwright browser installation (if configured) ────────────────────────── -# Add conditional Playwright browser installation if [[ "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then - if [[ -z "${PLAYWRIGHT_WS_URL}" ]]; then - echo "Installing Playwright browsers..." - playwright install chromium - playwright install-deps chromium - fi - - python -c "import nltk; nltk.download('punkt_tab')" + if [[ -z "${PLAYWRIGHT_WS_URL:-}" ]]; then + echo "Installing Playwright Chromium browser..." + playwright install chromium + playwright install-deps chromium + fi + python -c "import nltk; nltk.download('punkt_tab')" fi -if [ -n "${WEBUI_SECRET_KEY_FILE}" ]; then - KEY_FILE="${WEBUI_SECRET_KEY_FILE}" -else - KEY_FILE=".webui_secret_key" -fi +# ── Secret key setup ───────────────────────────────────────────────────────── +KEY_FILE="${WEBUI_SECRET_KEY_FILE:-.webui_secret_key}" PORT="${PORT:-8080}" HOST="${HOST:-0.0.0.0}" -if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then - echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable." - if ! [ -e "$KEY_FILE" ]; then - echo "Generating WEBUI_SECRET_KEY" - # Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one. - echo $(head -c 12 /dev/random | base64) > "$KEY_FILE" +if [[ -z "${WEBUI_SECRET_KEY:-}" && -z "${WEBUI_JWT_SECRET_KEY:-}" ]]; then + echo "No WEBUI_SECRET_KEY environment variable set, loading from file." + + if [[ ! -f "$KEY_FILE" ]]; then + echo "Generating new WEBUI_SECRET_KEY..." + head -c 12 /dev/random | base64 > "$KEY_FILE" fi - echo "Loading WEBUI_SECRET_KEY from $KEY_FILE" + echo "Loading WEBUI_SECRET_KEY from ${KEY_FILE}" WEBUI_SECRET_KEY=$(cat "$KEY_FILE") fi +# ── Ollama (bundled Docker image) ──────────────────────────────────────────── + if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then - echo "USE_OLLAMA is set to true, starting ollama serve." - ollama serve & + echo "Starting bundled ollama serve..." + ollama serve & fi +# ── CUDA library paths ────────────────────────────────────────────────────── + if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then - echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries." - export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib" + echo "CUDA enabled — extending LD_LIBRARY_PATH for torch/cudnn libraries." + export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib" fi -# Check if SPACE_ID is set, if so, configure for space -if [ -n "$SPACE_ID" ]; then - echo "Configuring for HuggingFace Space deployment" - if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then - echo "Admin user configured, creating" - WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" & +# ── HuggingFace Space deployment ───────────────────────────────────────────── + +if [[ -n "${SPACE_ID:-}" ]]; then + echo "Configuring for HuggingFace Space deployment..." + + if [[ -n "${ADMIN_USER_EMAIL:-}" && -n "${ADMIN_USER_PASSWORD:-}" ]]; then + echo "Creating admin user for Space..." + WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-}" \ + uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" & webui_pid=$! - echo "Waiting for webui to start..." - while ! curl -s "http://localhost:${PORT}/health" > /dev/null; do + + echo "Waiting for server to become healthy..." + until curl -sf "http://localhost:${PORT}/health" > /dev/null 2>&1; do sleep 1 done - echo "Creating admin user..." - curl \ - -X POST "http://localhost:${PORT}/api/v1/auths/signup" \ - -H "accept: application/json" \ + + echo "Registering admin user..." + curl -sS -X POST "http://localhost:${PORT}/api/v1/auths/signup" \ + -H "Accept: application/json" \ -H "Content-Type: application/json" \ - -d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }" - echo "Shutting down webui..." - kill $webui_pid + -d "{\"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\"}" + + echo "Restarting server..." + kill "$webui_pid" + wait "$webui_pid" 2>/dev/null || true fi - export WEBUI_URL=${SPACE_HOST} + export WEBUI_URL="${SPACE_HOST}" fi +# ── Launch uvicorn ─────────────────────────────────────────────────────────── + PYTHON_CMD=$(command -v python3 || command -v python) UVICORN_WORKERS="${UVICORN_WORKERS:-1}" -# If script is called with arguments, use them; otherwise use default workers -if [ "$#" -gt 0 ]; then - ARGS=("$@") +if [[ "$#" -gt 0 ]]; then + ARGS=("$@") else - ARGS=(--workers "$UVICORN_WORKERS") + ARGS=(--workers "$UVICORN_WORKERS") fi -# Run uvicorn -WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app \ +exec env WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-}" \ + "$PYTHON_CMD" -m uvicorn open_webui.main:app \ --host "$HOST" \ --port "$PORT" \ --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \ diff --git a/confirm_remove.sh b/confirm_remove.sh deleted file mode 100755 index 051908e6de..0000000000 --- a/confirm_remove.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]" -read ans -if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then - command docker-compose 2>/dev/null - if [ "$?" == "0" ]; then - docker-compose down -v - else - docker compose down -v - fi -else - echo "Operation cancelled." -fi diff --git a/docker-cleanup.sh b/docker-cleanup.sh new file mode 100755 index 0000000000..5092b31c10 --- /dev/null +++ b/docker-cleanup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Tear down the compose project and remove all volumes (including data). +# --------------------------------------------------------------------------- + +echo "WARNING: This will stop all containers and delete all volumes (including persistent data)." +read -rp "Are you sure you want to continue? [y/N]: " answer + +if [[ "${answer,,}" =~ ^y(es)?$ ]]; then + docker compose down -v + echo "All containers and volumes have been removed." +else + echo "Operation cancelled." +fi diff --git a/docker-compose-launcher.sh b/docker-compose-launcher.sh new file mode 100755 index 0000000000..8305a10346 --- /dev/null +++ b/docker-compose-launcher.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Interactive docker compose launcher for Open WebUI. +# Supports GPU auto-detection, configurable ports, data mounts, and Playwright. +# --------------------------------------------------------------------------- + +readonly BOLD='\033[1m' +readonly GREEN='\033[1;32m' +readonly WHITE='\033[1;37m' +readonly RED='\033[0;31m' +readonly RESET='\033[0m' +readonly CHECK_MARK='\xE2\x9C\x93' + +# ── GPU detection ───────────────────────────────────────────────────────────── + +detect_gpu_driver() { + if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then + echo "nvidia" + elif lspci 2>/dev/null | grep -qi 'vga.*amd\|display.*amd'; then + if lspci | grep -qiE 'Radeon (RX|R[579]|HD [78])'; then + echo "amdgpu" + else + echo "radeon" + fi + elif lspci 2>/dev/null | grep -qi 'vga.*intel\|display.*intel'; then + echo "i915" + else + echo >&2 "Error: No supported GPU detected." + return 1 + fi +} + +# ── Argument helpers ────────────────────────────────────────────────────────── + +extract_bracket_value() { + local input="$1" fallback="${2:-}" + if [[ "$input" =~ \[.*=(.*)\] ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "$fallback" + fi +} + +usage() { + cat <&2 "Error: Invalid GPU count '${gpu_count}'. Must be a number or 'all'." + exit 1 + fi + export OLLAMA_GPU_DRIVER + OLLAMA_GPU_DRIVER=$(detect_gpu_driver) + export OLLAMA_GPU_COUNT="$gpu_count" + compose_files+=("-f" "docker-compose.gpu.yaml") +fi + +if [[ "$enable_api" == true ]]; then + export OLLAMA_WEBAPI_PORT="$api_port" + compose_files+=("-f" "docker-compose.api.yaml") +fi + +if [[ -n "$data_dir" ]]; then + export OLLAMA_DATA_DIR="$data_dir" + compose_files+=("-f" "docker-compose.data.yaml") +fi + +if [[ "$enable_playwright" == true ]]; then + compose_files+=("-f" "docker-compose.playwright.yaml") +fi + +export OPEN_WEBUI_PORT="$webui_port" + +up_args=("up" "-d" "--remove-orphans" "--force-recreate") +if [[ "$build_image" == true ]]; then + up_args+=("--build") +fi + +# ── Confirmation ────────────────────────────────────────────────────────────── + +echo +echo -e "${WHITE}${BOLD}Current Setup:${RESET}" +echo -e " ${GREEN}${BOLD}GPU Driver:${RESET} ${OLLAMA_GPU_DRIVER:-Disabled}" +echo -e " ${GREEN}${BOLD}GPU Count:${RESET} ${OLLAMA_GPU_COUNT:-Disabled}" +echo -e " ${GREEN}${BOLD}API Port:${RESET} ${OLLAMA_WEBAPI_PORT:-Disabled}" +echo -e " ${GREEN}${BOLD}Data Dir:${RESET} ${data_dir:-Docker volume}" +echo -e " ${GREEN}${BOLD}WebUI Port:${RESET} ${webui_port}" +echo -e " ${GREEN}${BOLD}Playwright:${RESET} ${enable_playwright}" +echo + +if [[ "$headless" != true ]]; then + read -rp "$(echo -e "${WHITE}${BOLD}Proceed with this setup? [Y/n]: ${RESET}")" confirm + if [[ "${confirm,,}" == "n" ]]; then + echo "Aborted." + exit 0 + fi +fi + +# ── Launch ──────────────────────────────────────────────────────────────────── + +if docker compose "${compose_files[@]}" "${up_args[@]}"; then + echo + echo -e "${GREEN}${BOLD}${CHECK_MARK} Compose project started successfully.${RESET}" +else + echo + echo -e "${RED}${BOLD}Failed to start compose project.${RESET}" + exit 1 +fi + +echo diff --git a/docker-ollama.sh b/docker-ollama.sh new file mode 100755 index 0000000000..bf9cf6046d --- /dev/null +++ b/docker-ollama.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Pull and run the official Ollama container with optional GPU support. +# --------------------------------------------------------------------------- + +readonly CONTAINER="ollama" +readonly HOST_PORT="${OLLAMA_PORT:-11434}" +readonly CONTAINER_PORT=11434 + +read -rp "Enable GPU passthrough? [y/N]: " use_gpu + +echo "Pulling latest Ollama image..." +docker pull ollama/ollama:latest + +echo "Stopping any existing ${CONTAINER} container..." +docker rm -f "$CONTAINER" 2>/dev/null || true + +gpu_flags=() +if [[ "${use_gpu,,}" =~ ^y(es)?$ ]]; then + gpu_flags=("--gpus=all") + echo "GPU passthrough enabled." +fi + +echo "Starting ${CONTAINER}..." +docker run -d \ + "${gpu_flags[@]}" \ + -v "ollama:/root/.ollama" \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + --name "$CONTAINER" \ + ollama/ollama + +echo "Cleaning up dangling images..." +docker image prune -f + +echo "Ollama is running at http://localhost:${HOST_PORT}" diff --git a/docker-run.sh b/docker-run.sh new file mode 100755 index 0000000000..527c8d93e2 --- /dev/null +++ b/docker-run.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Build and run the Open WebUI Docker container locally. +# --------------------------------------------------------------------------- + +readonly IMAGE="open-webui" +readonly CONTAINER="open-webui" +readonly HOST_PORT="${OPEN_WEBUI_PORT:-3000}" +readonly CONTAINER_PORT=8080 + +echo "Building ${IMAGE} image..." +docker build -t "$IMAGE" . + +echo "Stopping any existing ${CONTAINER} container..." +docker stop "$CONTAINER" 2>/dev/null || true +docker rm "$CONTAINER" 2>/dev/null || true + +echo "Starting ${CONTAINER}..." +docker run -d \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + --add-host=host.docker.internal:host-gateway \ + -v "${IMAGE}:/app/backend/data" \ + --name "$CONTAINER" \ + --restart always \ + "$IMAGE" + +echo "Cleaning up dangling images..." +docker image prune -f + +echo "Open WebUI is running at http://localhost:${HOST_PORT}" diff --git a/docker-update-models.sh b/docker-update-models.sh new file mode 100755 index 0000000000..5f21715618 --- /dev/null +++ b/docker-update-models.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Pull the latest version of every model installed in the Ollama container. +# --------------------------------------------------------------------------- + +readonly CONTAINER="${OLLAMA_CONTAINER:-ollama}" + +echo "Fetching installed models from '${CONTAINER}' container..." +models=$(docker exec "$CONTAINER" ollama list | tail -n +2 | awk '{print $1}') + +if [[ -z "$models" ]]; then + echo "No models found." + exit 0 +fi + +echo "Updating models..." +while IFS= read -r model; do + echo " Pulling ${model}..." + docker exec "$CONTAINER" ollama pull "$model" +done <<< "$models" + +echo "All models updated." diff --git a/run-compose.sh b/run-compose.sh deleted file mode 100755 index 4fafedc6f7..0000000000 --- a/run-compose.sh +++ /dev/null @@ -1,250 +0,0 @@ -#!/bin/bash - -# Define color and formatting codes -BOLD='\033[1m' -GREEN='\033[1;32m' -WHITE='\033[1;37m' -RED='\033[0;31m' -NC='\033[0m' # No Color -# Unicode character for tick mark -TICK='\u2713' - -# Detect GPU driver -get_gpu_driver() { - # Detect NVIDIA GPUs using lspci or nvidia-smi - if lspci | grep -i nvidia >/dev/null || nvidia-smi >/dev/null 2>&1; then - echo "nvidia" - return - fi - - # Detect AMD GPUs (including GCN architecture check for amdgpu vs radeon) - if lspci | grep -i amd >/dev/null; then - # List of known GCN and later architecture cards - # This is a simplified list, and in a real-world scenario, you'd want a more comprehensive one - local gcn_and_later=("Radeon HD 7000" "Radeon HD 8000" "Radeon R5" "Radeon R7" "Radeon R9" "Radeon RX") - - # Get GPU information - local gpu_info=$(lspci | grep -i 'vga.*amd') - - for model in "${gcn_and_later[@]}"; do - if echo "$gpu_info" | grep -iq "$model"; then - echo "amdgpu" - return - fi - done - - # Default to radeon if no GCN or later architecture is detected - echo "radeon" - return - fi - - # Detect Intel GPUs - if lspci | grep -i intel >/dev/null; then - echo "i915" - return - fi - - # If no known GPU is detected - echo "Unknown or unsupported GPU driver" - exit 1 -} - -# Function for rolling animation -show_loading() { - local spin='-\|/' - local i=0 - - printf " " - - while kill -0 $1 2>/dev/null; do - i=$(( (i+1) %4 )) - printf "\b${spin:$i:1}" - sleep .1 - done - - # Replace the spinner with a tick - printf "\b${GREEN}${TICK}${NC}" -} - -# Usage information -usage() { - echo "Usage: $0 [OPTIONS]" - echo "Options:" - echo " --enable-gpu[count=COUNT] Enable GPU support with the specified count." - echo " --enable-api[port=PORT] Enable API and expose it on the specified port." - echo " --webui[port=PORT] Set the port for the web user interface." - echo " --data[folder=PATH] Bind mount for ollama data folder (by default will create the 'ollama' volume)." - echo " --playwright Enable Playwright support for web scraping." - echo " --build Build the docker image before running the compose project." - echo " --drop Drop the compose project." - echo " -q, --quiet Run script in headless mode." - echo " -h, --help Show this help message." - echo "" - echo "Examples:" - echo " $0 --drop" - echo " $0 --enable-gpu[count=1]" - echo " $0 --enable-gpu[count=all]" - echo " $0 --enable-api[port=11435]" - echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]" - echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]" - echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data] --build" - echo "" - echo "This script configures and runs a docker-compose setup with optional GPU support, API exposure, and web UI configuration." - echo "About the gpu to use, the script automatically detects it using the "lspci" command." - echo "In this case the gpu detected is: $(get_gpu_driver)" -} - -# Default values -gpu_count=1 -api_port=11435 -webui_port=3000 -headless=false -build_image=false -kill_compose=false -enable_playwright=false - -# Function to extract value from the parameter -extract_value() { - echo "$1" | sed -E 's/.*\[.*=(.*)\].*/\1/; t; s/.*//' -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - key="$1" - - case $key in - --enable-gpu*) - enable_gpu=true - value=$(extract_value "$key") - gpu_count=${value:-1} - ;; - --enable-api*) - enable_api=true - value=$(extract_value "$key") - api_port=${value:-11435} - ;; - --webui*) - value=$(extract_value "$key") - webui_port=${value:-3000} - ;; - --data*) - value=$(extract_value "$key") - data_dir=${value:-"./ollama-data"} - ;; - --playwright) - enable_playwright=true - ;; - --drop) - kill_compose=true - ;; - --build) - build_image=true - ;; - -q|--quiet) - headless=true - ;; - -h|--help) - usage - exit - ;; - *) - # Unknown option - echo "Unknown option: $key" - usage - exit 1 - ;; - esac - shift # past argument or value -done - -if [[ $kill_compose == true ]]; then - docker compose down --remove-orphans - echo -e "${GREEN}${BOLD}Compose project dropped successfully.${NC}" - exit -else - DEFAULT_COMPOSE_COMMAND="docker compose -f docker-compose.yaml" - if [[ $enable_gpu == true ]]; then - # Validate and process command-line arguments - if [[ -n $gpu_count ]]; then - if ! [[ $gpu_count =~ ^([0-9]+|all)$ ]]; then - echo "Invalid GPU count: $gpu_count" - exit 1 - fi - echo "Enabling GPU with $gpu_count GPUs" - # Add your GPU allocation logic here - export OLLAMA_GPU_DRIVER=$(get_gpu_driver) - export OLLAMA_GPU_COUNT=$gpu_count # Set OLLAMA_GPU_COUNT environment variable - fi - DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.gpu.yaml" - fi - if [[ $enable_api == true ]]; then - DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.api.yaml" - if [[ -n $api_port ]]; then - export OLLAMA_WEBAPI_PORT=$api_port # Set OLLAMA_WEBAPI_PORT environment variable - fi - fi - if [[ -n $data_dir ]]; then - DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml" - export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable - fi - if [[ $enable_playwright == true ]]; then - DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.playwright.yaml" - fi - if [[ -n $webui_port ]]; then - export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable - fi - DEFAULT_COMPOSE_COMMAND+=" up -d" - DEFAULT_COMPOSE_COMMAND+=" --remove-orphans" - DEFAULT_COMPOSE_COMMAND+=" --force-recreate" - if [[ $build_image == true ]]; then - DEFAULT_COMPOSE_COMMAND+=" --build" - fi -fi - -# Recap of environment variables -echo -echo -e "${WHITE}${BOLD}Current Setup:${NC}" -echo -e " ${GREEN}${BOLD}GPU Driver:${NC} ${OLLAMA_GPU_DRIVER:-Not Enabled}" -echo -e " ${GREEN}${BOLD}GPU Count:${NC} ${OLLAMA_GPU_COUNT:-Not Enabled}" -echo -e " ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}" -echo -e " ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}" -echo -e " ${GREEN}${BOLD}WebUI Port:${NC} $webui_port" -echo -e " ${GREEN}${BOLD}Playwright:${NC} ${enable_playwright:-false}" -echo - -if [[ $headless == true ]]; then - echo -ne "${WHITE}${BOLD}Running in headless mode... ${NC}" - choice="y" -else - # Ask for user acceptance - echo -ne "${WHITE}${BOLD}Do you want to proceed with current setup? (Y/n): ${NC}" - read -n1 -s choice -fi - -echo - -if [[ $choice == "" || $choice == "y" ]]; then - # Execute the command with the current user - eval "$DEFAULT_COMPOSE_COMMAND" & - - # Capture the background process PID - PID=$! - - # Display the loading animation - #show_loading $PID - - # Wait for the command to finish - wait $PID - - echo - # Check exit status - if [ $? -eq 0 ]; then - echo -e "${GREEN}${BOLD}Compose project started successfully.${NC}" - else - echo -e "${RED}${BOLD}There was an error starting the compose project.${NC}" - fi -else - echo "Aborted." -fi - -echo diff --git a/run-ollama-docker.sh b/run-ollama-docker.sh deleted file mode 100644 index c2a025bea3..0000000000 --- a/run-ollama-docker.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -host_port=11434 -container_port=11434 - -read -r -p "Do you want ollama in Docker with GPU support? (y/n): " use_gpu - -docker rm -f ollama || true -docker pull ollama/ollama:latest - -docker_args="-d -v ollama:/root/.ollama -p $host_port:$container_port --name ollama ollama/ollama" - -if [ "$use_gpu" = "y" ]; then - docker_args="--gpus=all $docker_args" -fi - -docker run $docker_args - -docker image prune -f \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100644 index 6793fe1627..0000000000 --- a/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -image_name="open-webui" -container_name="open-webui" -host_port=3000 -container_port=8080 - -docker build -t "$image_name" . -docker stop "$container_name" &>/dev/null || true -docker rm "$container_name" &>/dev/null || true - -docker run -d -p "$host_port":"$container_port" \ - --add-host=host.docker.internal:host-gateway \ - -v "${image_name}:/app/backend/data" \ - --name "$container_name" \ - --restart always \ - "$image_name" - -docker image prune -f diff --git a/update_ollama_models.sh b/update_ollama_models.sh deleted file mode 100755 index bde11b4b24..0000000000 --- a/update_ollama_models.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# update_llm.sh - -# Retrieves the list of LLMs installed in the Docker container -llm_list=$(docker exec ollama ollama list | tail -n +2 | awk '{print $1}') - -# Loop over each LLM to update it -for llm in $llm_list; do - docker exec ollama ollama pull $llm -done