name: Docker Build, Push, and Deploy to Portainer on: push: branches: - main env: REGISTRY_TOKEN: ${{ secrets.GLOBAL_PACKAGE_TOKEN }} REGISTRY_USER: ${{ vars.GLOBAL_ACCOUNT_EMAIL }} jobs: build-and-push: runs-on: ubuntu-latest name: Build and Push Docker Image steps: - name: Checkout code uses: actions/checkout@v4 - name: Login to Gitea Package Regsitry uses: docker/login-action@v3 with: registry: ${{ vars.REGISTRY_URL }} username: ${{ env.REGISTRY_USER }} password: ${{ env.REGISTRY_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and Push Docker Image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile push: true tags: | ${{ vars.REGISTRY_URL }}/${{ gitea.repository }}:latest ${{ vars.REGISTRY_URL }}/${{ gitea.repository }}:${{ gitea.sha }} - name: Logout from Gitea Package Regsitry if: always() run: docker logout ${{vars.REGISTRY_URL}} deploy-to-portainer: runs-on: ubuntu-latest name: Deploy Application to Portainer needs: build-and-push env: PORTAINER_URL: ${{ secrets.PORTAINER_URL }} PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} PORTAINER_ENVIRONMENT_ID: ${{ secrets.PORTAINER_ENVIRONMENT_ID }} IMAGE_TO_DEPLOY: ${{ vars.REGISTRY_URL }}/${{ gitea.repository }}:latest IMAGE_NAME: ${{ gitea.repository }}:latest CONTAINER_NAME: ${{ vars.PORTAINER_CONTAINER_NAME || 'deep-research' }} # Name of the container in Portainer HOST_PORT: "8005" # Host port to map to container's 3000 CONTAINER_PORT: "3000" # Container port RESTART_POLICY: "unless-stopped" # For curl: -k allows insecure connections (e.g. self-signed certs). Omit for production with valid certs. # CURL_OPTS: "-s -k" # If you need -k CURL_OPTS: "-s" # Silent mode steps: - name: Deploy Container to Portainer via API run: | set -e # Exit immediately if a command exits with a non-zero status. echo "Starting deployment of image ${{ env.IMAGE_TO_DEPLOY }} as container ${{ env.CONTAINER_NAME }}" PORTAINER_API_BASE="${{ env.PORTAINER_URL }}/api/endpoints/${{ env.PORTAINER_ENVIRONMENT_ID }}" PORTAINER_API_DOCKER_BASE="${PORTAINER_API_BASE}/docker" AUTH_HEADER="X-API-Key: ${{ env.PORTAINER_API_KEY }}" # 0. Explicitly pull the image using Portainer's configured registry credentials echo "Attempting to pull image '${{ env.IMAGE_TO_DEPLOY }}' via Portainer..." IMAGE_FULL_NAME_FOR_PULL="${{ env.IMAGE_TO_DEPLOY }}" # Extract image name (everything before the last colon) and tag (everything after the last colon) IMAGE_NAME_NO_TAG_FOR_PULL=$(echo "$IMAGE_FULL_NAME_FOR_PULL" | sed -n 's/\(.*\):\([^:]*\)/\1/p') IMAGE_TAG_FOR_PULL=$(echo "$IMAGE_FULL_NAME_FOR_PULL" | sed -n 's/\(.*\):\([^:]*\)/\2/p') # If sed didn't find a tag (e.g., image name had no colon, or only one part like 'imagename'), assume 'latest' if [ -z "$IMAGE_TAG_FOR_PULL" ] || [ "$IMAGE_NAME_NO_TAG_FOR_PULL" == "" ]; then IMAGE_TAG_FOR_PULL="latest" IMAGE_NAME_NO_TAG_FOR_PULL="$IMAGE_FULL_NAME_FOR_PULL" # Assume full name was without tag if parsing failed if echo "$IMAGE_NAME_NO_TAG_FOR_PULL" | grep -q ':'; then # If it still contains ':', it's likely it was just 'image:tag' and parsing failed above IMAGE_NAME_NO_TAG_FOR_PULL=$(echo "$IMAGE_FULL_NAME_FOR_PULL" | cut -d: -f1) # fallback for simple 'image:tag' fi fi echo "Pulling image name: '$IMAGE_NAME_NO_TAG_FOR_PULL' with tag: '$IMAGE_TAG_FOR_PULL'" HTTP_CODE_PULL=$(curl ${{ env.CURL_OPTS }} -w "%{http_code}" -o /tmp/portainer_pull_response.txt -X POST \ -H "$AUTH_HEADER" \ "${PORTAINER_API_BASE}/images/create?fromImage=${IMAGE_NAME_NO_TAG_FOR_PULL}&tag=${IMAGE_TAG_FOR_PULL}") PULL_RESPONSE_BODY=$(cat /tmp/portainer_pull_response.txt) if [ "$HTTP_CODE_PULL" -eq 200 ]; then echo "Image pull initiated successfully by Portainer (HTTP 200)." echo "Portainer pull response (first 10 lines):"; head -n 10 /tmp/portainer_pull_response.txt echo "Waiting 60 seconds for image pull to complete..." sleep 60 elif [ "$HTTP_CODE_PULL" -eq 401 ] || [ "$HTTP_CODE_PULL" -eq 403 ] || [ "$HTTP_CODE_PULL" -eq 500 ]; then echo "Error during image pull (HTTP $HTTP_CODE_PULL). This strongly indicates an AUTHENTICATION FAILURE with the Gitea registry via Portainer." echo "Please RE-VERIFY the credentials (username/token and its permissions) for '${{ vars.REGISTRY_URL }}' in Portainer's Registries section." echo "Response body: $PULL_RESPONSE_BODY"; exit 1 elif [ "$HTTP_CODE_PULL" -eq 404 ]; then echo "Error during image pull (HTTP $HTTP_CODE_PULL). Image '${IMAGE_NAME_NO_TAG_FOR_PULL}:${IMAGE_TAG_FOR_PULL}' not found in the Gitea registry."; echo "Response body: $PULL_RESPONSE_BODY"; exit 1 else echo "Error initiating image pull. Portainer responded with HTTP code: $HTTP_CODE_PULL"; echo "Response body: $PULL_RESPONSE_BODY"; exit 1 fi # 1. Find existing container by name echo "Searching for existing container named '${{ env.CONTAINER_NAME }}'..." # Portainer API often prepends '/' to container names in the .Names field. # Using jq to filter based on any name in the .Names array matching / EXISTING_CONTAINER_ID=$(curl ${{ env.CURL_OPTS }} -H "$AUTH_HEADER" \ "${PORTAINER_API_DOCKER_BASE}/containers/json?all=true" | \ jq -r --arg CN "/${{ env.CONTAINER_NAME }}" '.[] | select(.Names[] | contains($CN)) | .Id' | head -n 1) # 2. If container exists, stop and remove it if [ -n "$EXISTING_CONTAINER_ID" ]; then echo "Found existing container '${{ env.CONTAINER_NAME }}' with ID '$EXISTING_CONTAINER_ID'." echo "Stopping container..." curl ${{ env.CURL_OPTS }} -X POST -H "$AUTH_HEADER" "${PORTAINER_API_DOCKER_BASE}/containers/${EXISTING_CONTAINER_ID}/stop" echo "Waiting for container to stop..." sleep 5 # Adjust sleep time as needed echo "Removing container..." curl ${{ env.CURL_OPTS }} -X DELETE -H "$AUTH_HEADER" "${PORTAINER_API_DOCKER_BASE}/containers/${EXISTING_CONTAINER_ID}" echo "Container '${{ env.CONTAINER_NAME }}' removed." else echo "No existing container named '${{ env.CONTAINER_NAME }}' found." fi # 3. Create new container echo "Creating new container '${{ env.CONTAINER_NAME }}' with image '${{ env.IMAGE_TO_DEPLOY }}'..." CREATE_PAYLOAD=$(cat <