chore: harden webapp docker image (#3845)

Hardens the webapp Docker image and adds a CVE scan of each published
image.

- Base image `bullseye-slim` → `bookworm-slim` (Debian 12), pinned by
digest. Adds `apt-get upgrade` + `--no-install-recommends` + apt-cache
cleanup across the build stages so OS packages are patched at build
time.
- Moves the `react-email` CLI to `devDependencies` in
`internal-packages/emails` — only the `email dev` preview script uses
it; the runtime render path is `@react-email/render` +
`@react-email/components`. This also drops the bundled `esbuild` binary
from the production image.
- Bumps `goose` v3.26.0 → v3.27.1 and its Go builder image 1.23 → 1.26.
- Adds a reusable Trivy image-scan workflow wired into `publish.yml`, so
every published image (main builds and releases) is scanned for
OS-package CVEs right after it's pushed to GHCR. Report-only (writes to
the run summary), runs alongside the worker publishes so it never blocks
a deploy.

Verified locally: the image builds clean on the new base, and
`@react-email/render` carries no `esbuild` dependency so email rendering
is unaffected.
This commit is contained in:
nicktrn
2026-06-05 17:52:42 +01:00
committed by GitHub
parent 96f4c1bf2c
commit 16d59aa9e7
6 changed files with 104 additions and 11 deletions
+7
View File
@@ -14,6 +14,13 @@ on:
type: string
required: false
default: ""
outputs:
version:
description: The published image tag
value: ${{ jobs.publish.outputs.version }}
short_sha:
description: Short commit SHA of the published build
value: ${{ jobs.publish.outputs.short_sha }}
secrets:
SENTRY_AUTH_TOKEN:
required: false
+11
View File
@@ -96,3 +96,14 @@ jobs:
uses: ./.github/workflows/publish-worker-v4.yml
with:
image_tag: ${{ inputs.image_tag }}
# OS-level CVE scan of the image just published above. Report-only (writes to
# the run summary); runs alongside the worker publishes and never blocks them.
scan-webapp:
needs: [publish-webapp]
permissions:
contents: read
packages: read # pull the just-published image from GHCR
uses: ./.github/workflows/trivy-image-webapp.yml
with:
image-ref: ghcr.io/triggerdotdev/trigger.dev:${{ needs.publish-webapp.outputs.version }}
+75
View File
@@ -0,0 +1,75 @@
name: Trivy Image Scan (webapp)
# OS-level CVE scan of a published webapp image. Called by the publish pipeline
# (publish.yml) to scan each build right after it's pushed to GHCR — so every
# main build and every release is scanned, not rebuilt. Also runnable ad-hoc
# via workflow_dispatch against any image ref.
#
# Report-only: writes a table to the run summary. No SARIF upload, no gate.
# Library/dependency CVEs are covered by Dependabot, so this is restricted to
# OS packages (`vuln-type: os`) to avoid double-reporting.
on:
workflow_call:
inputs:
image-ref:
description: "Full image ref to scan (e.g. ghcr.io/triggerdotdev/trigger.dev:main)"
type: string
required: true
workflow_dispatch:
inputs:
image-ref:
description: "Full image ref to scan"
type: string
required: false
default: "ghcr.io/triggerdotdev/trigger.dev:main"
permissions: {}
concurrency:
group: trivy-image-webapp-${{ inputs.image-ref }}
cancel-in-progress: true
jobs:
scan:
name: Scan
runs-on: ubuntu-latest
permissions:
contents: read
packages: read # pull the image from GHCR
steps:
# Authenticate to GHCR so the scan also works for private images
# (GITHUB_TOKEN isn't forwarded to Docker automatically). Harmless for
# public images. Pairs with the packages: read permission above.
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy image scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: image
image-ref: ${{ inputs.image-ref }}
# vuln-type maps to --pkg-types: OS packages only (library deps are
# Dependabot's job). ignore-unfixed drops vulns with no patch yet.
vuln-type: os
ignore-unfixed: true
severity: HIGH,CRITICAL
format: table
output: trivy-image-webapp.txt
- name: Job summary
if: always()
env:
IMAGE_REF: ${{ inputs.image-ref }}
run: |
{
echo "## Trivy Image Scan (webapp) — \`${IMAGE_REF}\`"
echo '```'
# GitHub step summary is capped at 1 MiB; truncate large reports.
head -c 900000 trivy-image-webapp.txt 2>/dev/null || echo "(no report produced)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
+6 -6
View File
@@ -1,7 +1,7 @@
ARG NODE_IMAGE=node:20.20.2-bullseye-slim@sha256:65ef49f7d24aefd012a7fc6f9a2b734bcc19e424976a81f60c86b47266ef5b28
ARG NODE_IMAGE=node:20.20.2-bookworm-slim@sha256:2cf067cfed83d5ea958367df9f966191a942351a2df77d6f0193e162b5febfc0
FROM golang:1.23-alpine AS goose_builder
RUN go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
FROM golang:1.26-alpine AS goose_builder
RUN go install github.com/pressly/goose/v3/cmd/goose@v3.27.1
FROM ${NODE_IMAGE} AS pruner
@@ -13,7 +13,7 @@ RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
# Base strategy to have layer caching
FROM ${NODE_IMAGE} AS base
RUN apt-get update && apt-get install -y openssl dumb-init
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends openssl dumb-init && rm -rf /var/lib/apt/lists/*
WORKDIR /triggerdotdev
COPY --chown=node:node .gitignore .gitignore
COPY --from=pruner --chown=node:node /triggerdotdev/out/json/ .
@@ -43,7 +43,7 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install
## Builder (builds the webapp)
FROM base AS builder
# This is needed for the sentry-cli binary while building the webapp
RUN apt-get update && apt-get install -y openssl dumb-init ca-certificates
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends openssl dumb-init ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /triggerdotdev
# Corepack is used to install pnpm with the exact version from packageManager
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
@@ -75,7 +75,7 @@ RUN --mount=type=secret,id=sentry_auth_token \
# Runner
FROM ${NODE_IMAGE} AS runner
RUN apt-get update && apt-get install -y openssl netcat-openbsd ca-certificates
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends openssl netcat-openbsd ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /triggerdotdev
ENV NODE_ENV=production
+2 -2
View File
@@ -16,7 +16,6 @@
"nodemailer": "^8.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-email": "^6.5.0",
"resend": "^3.2.0",
"tiny-invariant": "^1.2.0",
"zod": "3.25.76"
@@ -24,7 +23,8 @@
"devDependencies": {
"@types/nodemailer": "^8.0.0",
"@types/react": "18.2.69",
"@types/react-dom": "18.2.7"
"@types/react-dom": "18.2.7",
"react-email": "^6.5.0"
},
"engines": {
"node": ">=18.0.0"
+3 -3
View File
@@ -1170,9 +1170,6 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.3.1)
react-email:
specifier: ^6.5.0
version: 6.5.0(bufferutil@4.0.9)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
resend:
specifier: ^3.2.0
version: 3.2.0
@@ -1192,6 +1189,9 @@ importers:
'@types/react-dom':
specifier: 18.2.7
version: 18.2.7
react-email:
specifier: ^6.5.0
version: 6.5.0(bufferutil@4.0.9)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
internal-packages/llm-model-catalog:
dependencies: