name: CI on: workflow_dispatch: push: branches: - dev - master paths-ignore: - "docs/**" # only run the latest commit to avoid cache overwrites concurrency: group: ${{ github.ref }} cancel-in-progress: true env: PYTHON_VERSION: 3.11 jobs: amd64_build: runs-on: ubuntu-22.04 name: AMD64 Build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push amd64 standard build uses: docker/build-push-action@v7 with: context: . file: docker/main/Dockerfile push: true platforms: linux/amd64 target: frigate tags: ${{ steps.setup.outputs.image-name }}-amd64 cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64 cache-to: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64,mode=max smoke_test: runs-on: ubuntu-22.04 name: AMD64 Smoke Test needs: - amd64_build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Start container run: | mkdir -p /tmp/frigate-config /tmp/frigate-media printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config/config.yml # simulate a root-era install: root-owned 0600 jwt secret pre-exists docker run --rm -v /tmp/frigate-config:/config --entrypoint bash \ ${{ steps.setup.outputs.image-name }}-amd64 \ -c "python3 -c 'import secrets; open(\"/config/.jwt_secret\",\"w\").write(secrets.token_hex(64))' && chmod 600 /config/.jwt_secret && chown 0:0 /config/.jwt_secret" docker run -d --name frigate --shm-size 256m \ -v /tmp/frigate-config:/config \ -v /tmp/frigate-media:/media/frigate \ --mount type=tmpfs,target=/tmp/cache,tmpfs-size=100000000 \ -p 5000:5000 -p 8971:8971 \ ${{ steps.setup.outputs.image-name }}-amd64 - name: Wait for API run: | for i in $(seq 1 60); do curl -fs http://127.0.0.1:5000/api/version && exit 0 sleep 5 done echo "API never came up"; docker logs frigate; exit 1 - name: Assert security headers and permissions run: | headers=$(curl -ksI https://127.0.0.1:8971/) echo "$headers" echo "$headers" | grep -qi "x-content-type-options: nosniff" echo "$headers" | grep -qi "referrer-policy: strict-origin-when-cross-origin" # server_tokens off: Server header must not include a version. # written as an if rather than "! grep", because bash exempts a # negated command from set -e and the assertion would never fail if echo "$headers" | grep -qiE "^server: nginx/[0-9]"; then echo "Server header leaks the nginx version; server_tokens is not off" exit 1 fi # Frigate never ships frame-ancestors: HA's Webpage card and iframe # panels frame it cross-origin and it would break them silently if echo "$headers" | grep -qi "frame-ancestors"; then echo "response carries frame-ancestors, which breaks cross-origin iframe embedding" exit 1 fi # -t must NOT run as root: ngx_create_paths would chown the live cache # and temp dirs to the `user root` directive user, breaking the workers. # stdout goes to /dev/null because -t reopens the config's # error_log/access_log /dev/stdout by path, and the docker exec pipe # is root-owned; -t reports on stderr, so nothing is lost docker exec frigate /command/s6-setuidgid frigate bash -c '/usr/local/nginx/sbin/nginx -e stderr -t -c /tmp/nginx/conf/nginx.conf >/dev/null' docker exec frigate stat -c %a /etc/letsencrypt/live/frigate/privkey.pem | grep -qx 600 docker exec frigate stat -c %a /dev/shm/go2rtc.yaml | grep -qx 640 - name: Assert services run as non-root run: | ps_out=$(docker exec frigate ps -eo user=,comm=) echo "$ps_out" assert_nonroot() { # the process must exist AND no instance of it may run as root echo "$ps_out" | grep -qw "$1" || { echo "$1 is not running"; exit 1; } if echo "$ps_out" | grep -w "$1" | grep -q '^root'; then echo "$1 is running as root"; exit 1 fi } assert_nonroot python3 assert_nonroot go2rtc assert_nonroot nginx # root-era jwt secret must have been captured by the sweep and the # auth stack must be functional: wrong creds => clean 401, not 500 docker exec frigate stat -c %u /config/.jwt_secret | grep -qx "$(docker exec frigate id -u frigate)" code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:5000/api/login \ -H 'content-type: application/json' -d '{"user":"admin","password":"definitely-wrong"}') [ "$code" = "401" ] || { echo "login endpoint returned $code"; exit 1; } # nginx runtime state must belong to the runtime user (a root nginx -t # in the step above would have chowned it to root) owners=$(docker exec frigate stat -c %U /tmp/nginx /dev/shm/nginx_cache) echo "$owners" if echo "$owners" | grep -qvx frigate; then echo "nginx runtime dirs are not owned by frigate"; exit 1 fi # runtime user can write recordings storage docker exec frigate /command/s6-setuidgid frigate touch /media/frigate/.write-probe docker exec frigate rm /media/frigate/.write-probe # /tmp/cache is a tmpfs mount here, as the docs recommend: it arrives # root-owned, and the ZMQ IPC sockets live in it docker exec frigate /command/s6-setuidgid frigate touch /tmp/cache/.write-probe docker exec frigate rm /tmp/cache/.write-probe - name: Assert escape hatch restores root run: | mkdir -p /tmp/frigate-config-root printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-root/config.yml # pre-seed a sentinel: the assertion below is that the escape hatch # DELETES it. Against a fresh dir the absence check passes vacuously # and proves nothing about the rm -f in the prepare script. echo "2:1000:1000" > /tmp/frigate-config-root/.permissions_version docker run -d --name frigate-root --shm-size 256m \ -e FRIGATE_RUN_AS_ROOT=true \ -v /tmp/frigate-config-root:/config \ ${{ steps.setup.outputs.image-name }}-amd64 up=0 for i in $(seq 1 60); do docker exec frigate-root curl -fs http://127.0.0.1:5000/api/version && up=1 && break sleep 5 done if [ "$up" -ne 1 ]; then echo "escape hatch container never healthy"; docker logs frigate-root; exit 1; fi ps_out=$(docker exec frigate-root ps -eo user=,comm=) echo "$ps_out" echo "$ps_out" | grep -w python3 | grep -q '^root' echo "$ps_out" | grep -w go2rtc | grep -q '^root' echo "$ps_out" | grep -w nginx | grep -q '^root' # escape hatch must have DELETED the pre-seeded sentinel. written as # an if because bash exempts a negated command from set -e if docker exec frigate-root test -f /config/.permissions_version; then echo "escape hatch did not delete the sweep sentinel"; exit 1 fi docker rm -f frigate-root - name: Assert PUID/PGID remapping run: | mkdir -p /tmp/frigate-config-puid /tmp/frigate-media-puid printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-puid/config.yml docker run -d --name frigate-puid --shm-size 256m \ -e PUID=1500 -e PGID=1500 \ -v /tmp/frigate-config-puid:/config \ -v /tmp/frigate-media-puid:/media/frigate \ ${{ steps.setup.outputs.image-name }}-amd64 up=0 for i in $(seq 1 60); do docker exec frigate-puid curl -fs http://127.0.0.1:5000/api/version && up=1 && break sleep 5 done if [ "$up" -ne 1 ]; then echo "PUID container never became healthy"; docker logs frigate-puid; exit 1; fi docker exec frigate-puid id -u frigate | grep -qx 1500 docker exec frigate-puid id -g frigate | grep -qx 1500 docker exec frigate-puid cat /config/.permissions_version | grep -qx "2:1500:1500" # second boot must skip the sweep (sentinel hit). Poll rather than # sleep: the string can only come from the second boot (the first # had no sentinel), so grepping the full log is unambiguous. docker restart frigate-puid ok=0 for i in $(seq 1 30); do docker logs frigate-puid 2>&1 | grep -q "already applied" && ok=1 && break sleep 2 done if [ "$ok" -ne 1 ]; then echo "sentinel skip never logged"; docker logs frigate-puid; exit 1; fi docker rm -f frigate-puid - name: Teardown if: always() run: docker rm -f frigate || true arm64_build: runs-on: ubuntu-22.04-arm name: ARM Build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push arm64 standard build uses: docker/build-push-action@v7 with: context: . file: docker/main/Dockerfile push: true platforms: linux/arm64 target: frigate tags: | ${{ steps.setup.outputs.image-name }}-standard-arm64 cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 - name: Build and push RPi build uses: docker/bake-action@v7 with: source: . push: true targets: rpi files: docker/rpi/rpi.hcl set: | rpi.tags=${{ steps.setup.outputs.image-name }}-rpi *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max jetson_jp6_build: runs-on: ubuntu-22.04-arm name: Jetson Jetpack 6 steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push TensorRT (Jetson, Jetpack 6) env: ARCH: arm64 BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu SLIM_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu TRT_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu uses: docker/bake-action@v7 with: source: . push: true targets: tensorrt files: docker/tensorrt/trt.hcl set: | tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp6 *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6 *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6,mode=max amd64_extra_builds: runs-on: ubuntu-22.04 name: AMD64 Extra Build needs: - amd64_build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push TensorRT (x86 GPU) env: COMPUTE_LEVEL: "50 60 70 80 90" uses: docker/bake-action@v7 with: source: . push: true targets: tensorrt files: docker/tensorrt/trt.hcl set: | tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max - name: AMD/ROCm general build env: HSA_OVERRIDE: 0 uses: docker/bake-action@v7 with: source: . push: true targets: rocm files: docker/rocm/rocm.hcl set: | rocm.tags=${{ steps.setup.outputs.image-name }}-rocm *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm,mode=max *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm arm64_extra_builds: runs-on: ubuntu-22.04-arm name: ARM Extra Build needs: - arm64_build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Rockchip build uses: docker/bake-action@v7 with: source: . push: true targets: rk files: docker/rockchip/rk.hcl set: | rk.tags=${{ steps.setup.outputs.image-name }}-rk *.cache-from=type=gha synaptics_build: runs-on: ubuntu-22.04-arm name: Synaptics Build needs: - arm64_build steps: - name: Check out code uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx id: setup uses: ./.github/actions/setup with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Synaptics build uses: docker/bake-action@v7 with: source: . push: true targets: synaptics files: docker/synaptics/synaptics.hcl set: | synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics *.cache-from=type=gha # The majority of users running arm64 are rpi users, so the rpi # build should be the primary arm64 image assemble_default_build: runs-on: ubuntu-22.04 name: Assemble and push default build needs: - amd64_build - arm64_build steps: - id: lowercaseRepo uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository }} - name: Log in to the Container registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create short sha run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV - uses: int128/docker-manifest-create-action@v2 with: tags: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }} sources: | ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64 ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi