diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 0000000..b1e2a24 --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,142 @@ +name: Build Docker container image + +on: + push: + branches: + - 'main' + - 'beta' + - 'stable' + tags: + - 'v*' + pull_request: + merge_group: + workflow_dispatch: + inputs: + git-ref: + description: 'Git ref (optional)' + required: false + +env: + REGISTRY_IMAGE: ghcr.io/planktoscope/streamlit-classification-app + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Prepare environment variables + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + # Build and publish Docker container image + - name: Get Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=sha + + - 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 GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v5 + with: + context: ./ + pull: true + push: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) || github.event_name == 'push' || github.event_name == 'push tag' }} + platforms: ${{ matrix.platform }} + tags: ${{ env.REGISTRY_IMAGE }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} + + - 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-${{ 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 + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + # Build and publish Docker container image + - name: Get Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=match,pattern=v(.*),group=1 + type=edge,branch=main + type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'main') && github.ref != format('refs/heads/{0}', 'main') }} + type=ref,event=pr + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'stable') }} + type=sha + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + if: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) || github.event_name == 'push' || github.event_name == 'push tag' }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + if: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) || github.event_name == 'push' || github.event_name == 'push tag' }} + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + if: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) || github.event_name == 'push' || github.event_name == 'push tag' }} + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/Dockerfile b/Dockerfile index b26cd70..53c320b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,15 @@ # Use an official Python runtime as the base image -FROM python:3.12 +# We use the same base image as what the PlanktoScope segmenter's container image uses: +FROM docker.io/library/python:3.9.18-slim-bullseye + +# Install curl for container healthcheck +RUN \ + apt-get update && \ + apt-get -y upgrade && \ + apt-get -y install --no-install-recommends curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + rm -f /tmp/apt-packages # Set the working directory in the container WORKDIR /app @@ -8,7 +18,10 @@ WORKDIR /app COPY . . # Install the required dependencies -RUN pip3 install -r requirements.txt +RUN \ + pip3 install -r requirements.txt --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple && \ + pip3 cache purge && \ + rm -rf /root/.cache/pip # Make port 8501 available to anyone outside this container EXPOSE 8501 @@ -16,8 +29,5 @@ EXPOSE 8501 # Add a healthcheck to the container HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health -# Set the command to run the Streamlit app (what the image will do when it starts as a container) -#CMD ["streamlit", "run", "app_model.py"] - # Run the Streamlit app -ENTRYPOINT ["streamlit", "run", "app_model.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file +ENTRYPOINT ["streamlit", "run", "app_model.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true"] diff --git a/forklift-repository.yml b/forklift-repository.yml new file mode 100644 index 0000000..b964b06 --- /dev/null +++ b/forklift-repository.yml @@ -0,0 +1,6 @@ +forklift-version: v0.7.0 + +repository: + path: github.com/PlanktoScope/streamlit-classification-app + description: Forklift package for the PlanktoScope OS + readme-file: README.md diff --git a/pkg/compose-frontend.yml b/pkg/compose-frontend.yml new file mode 100644 index 0000000..5ab9ad1 --- /dev/null +++ b/pkg/compose-frontend.yml @@ -0,0 +1,17 @@ +services: + server: + networks: + - caddy-ingress + labels: + caddy: :80 + caddy.redir: /ps/streamlit-demo /ps/streamlit-demo/ + caddy.handle_path: /ps/streamlit-demo/* + caddy.handle_path.reverse_proxy: "{{upstreams 8501}}" + environment: + STREAMLIT_SERVER_BASE_URL_PATH: /ps/streamlit-demo + healthcheck: + test: curl --fail http://localhost:8501/ps/streamlit-demo/_stcore/health || exit 1 + +networks: + caddy-ingress: + external: true diff --git a/pkg/compose.yml b/pkg/compose.yml new file mode 100644 index 0000000..7a622d9 --- /dev/null +++ b/pkg/compose.yml @@ -0,0 +1,8 @@ +services: + server: + image: ghcr.io/planktoscope/streamlit-classification-app:sha-0703263 + +networks: + default: + name: none + external: true diff --git a/pkg/forklift-package.yml b/pkg/forklift-package.yml new file mode 100644 index 0000000..dac0195 --- /dev/null +++ b/pkg/forklift-package.yml @@ -0,0 +1,33 @@ +package: + description: Demo app with a pre-trained demo model for classifying segmented objects + maintainers: + # Note: this is the maintainer of the Forklift package, not the maintainer of the app itself + - name: Ethan Li + email: lietk12@gmail.com + license: Apache-2.0 + sources: + - https://github.com/PlanktoScope/streamlit-classification-app + +deployment: + compose-files: [compose.yml] + +features: + frontend: + description: Provides access to a browser landing page + compose-files: [compose-frontend.yml] + requires: + networks: + - description: Overlay network for Caddy to connect to upstream services + name: caddy-ingress + services: + - tags: [caddy-docker-proxy] + port: 80 + protocol: http + provides: + services: + - description: PlanktoScope documentation site + port: 80 + protocol: http + paths: + - /ps/streamlit-demo + - /ps/streamlit-demo/* diff --git a/requirements.txt b/requirements.txt index 4bdbd48..dd76147 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ streamlit -opencv-python +opencv-python-headless==4.6.0.66 numpy seaborn matplotlib plotly Pillow torch -torchvision \ No newline at end of file +torchvision