Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
# ─── .dockerignore — Issue #491 hardened image ───────────────────────────────
# Keep the build context minimal to speed up transfers and prevent secrets
# from leaking into layers.

# Git
.git
.gitignore
.gitattributes

# Node / build artefacts
node_modules
dist
coverage
.git
*.tsbuildinfo

# Environment files — never bake secrets into the image
.env
.env.*
!.env.example
!.env.mainnet.example
!.env.staging.example
!.env.testnet.example

# Logs
*.log
npm-debug.log*

# Test / CI artefacts
test
__tests__
*.spec.ts
*.e2e-spec.ts
jest.config.*
.jest-cache
stryker.conf.*
mutation-report

# Docker itself
Dockerfile*
docker-compose*.yml
.dockerignore

# Editor / OS noise
.DS_Store
.vscode
.idea
*.swp
*.swo

# Docs and repo metadata — not needed at runtime
docs
*.md
!README.md
CHANGELOG.md
SECURITY.md
CONTRIBUTING.md
CODE_OF_CONDUCT.md
CODEOWNERS
LICENSE

# CI — not needed inside the image
.github
.semgrep
.husky

# Deploy charts — not baked into the image
deploy

# Temporary / profiling
tmp
.tmp
*.heapsnapshot
*.cpuprofile
17 changes: 15 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ jobs:

- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0

# Issue #491 — QEMU for arm64 cross-compilation in the build job.
- name: Set up QEMU (arm64 cross-build)
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff9c9f7eb5ca85ccdb5 # v3.6.0
with:
platforms: linux/arm64

- name: Resolve the image reference
id: ref
run: echo "image=${REGISTRY}/${IMAGE_NAME}" >> "$GITHUB_OUTPUT"
Expand All @@ -115,18 +121,24 @@ jobs:
# for the SBOM, the signature, both attestations, and every deploy step
# downstream — so "we deployed what we signed" is true by construction
# rather than by convention.
- name: Build and push the image
# Issue #491 — Multi-arch distroless build with registry cache.
- name: Build and push the image (multi-arch, distroless)
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
provenance: false
sbom: false
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
build-args: |
NODE_VERSION=20
DISTROLESS_TAG=nodejs20-debian12
- name: Fail early if the push produced no digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
Expand Down Expand Up @@ -938,3 +950,4 @@ jobs:
set -euo pipefail
docker pull "$IMAGE"
docker tag "$IMAGE" vortex-backend:production

254 changes: 254 additions & 0 deletions .github/workflows/container-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
name: Container Smoke Tests

# Issue #491 — Per-arch container smoke tests
#
# Runs after every build on PRs and main pushes. Validates that the hardened
# distroless image:
# 1. Boots without a shell or package manager
# 2. Responds to /health/live (liveness probe)
# 3. Handles a minimal intent-creation flow (POST /api/v1/intents)
#
# Both amd64 and arm64 are tested. arm64 uses QEMU emulation via
# docker/setup-qemu-action because GitHub Actions standard runners are x86_64.
# The smoke test itself is lightweight (<60s) so QEMU overhead is acceptable.

on:
pull_request:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- prisma/schema.prisma
- .github/workflows/container-smoke.yml
push:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- prisma/schema.prisma

concurrency:
group: container-smoke-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
# ── Build the image once, export as a tarball ────────────────────────────────
build:
name: Build multi-arch image
runs-on: ubuntu-latest
outputs:
# Passed to smoke test jobs so they load the exact same tarball
cache-key: ${{ steps.cache-key.outputs.value }}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

- name: Set up QEMU (arm64 emulation)
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff9c9f7eb5ca85ccdb5 # v3.6.0
with:
platforms: linux/arm64

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0

- name: Compute cache key
id: cache-key
run: echo "value=container-smoke-${{ github.sha }}" >> "$GITHUB_OUTPUT"

- name: Build amd64 image and export
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64
push: false
load: true
tags: vortex-backend:smoke-amd64
cache-from: type=gha,scope=smoke-amd64
cache-to: type=gha,mode=max,scope=smoke-amd64
build-args: |
NODE_VERSION=20
DISTROLESS_TAG=nodejs20-debian12

- name: Save amd64 image tarball
run: docker save vortex-backend:smoke-amd64 | gzip > /tmp/smoke-amd64.tar.gz

- name: Upload amd64 tarball
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: smoke-amd64
path: /tmp/smoke-amd64.tar.gz
retention-days: 1

- name: Build arm64 image and export
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/arm64
push: false
load: false
outputs: type=docker,dest=/tmp/smoke-arm64.tar.gz
tags: vortex-backend:smoke-arm64
cache-from: type=gha,scope=smoke-arm64
cache-to: type=gha,mode=max,scope=smoke-arm64
build-args: |
NODE_VERSION=20
DISTROLESS_TAG=nodejs20-debian12

- name: Upload arm64 tarball
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: smoke-arm64
path: /tmp/smoke-arm64.tar.gz
retention-days: 1

# ── Trivy CVE gate ────────────────────────────────────────────────────────────
trivy:
name: Trivy CVE scan (amd64)
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

- name: Download amd64 tarball
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: smoke-amd64
path: /tmp

- name: Load image
run: docker load < /tmp/smoke-amd64.tar.gz

- name: Run Trivy — fail on HIGH/CRITICAL with a fix available
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.30.0
with:
image-ref: vortex-backend:smoke-amd64
format: table
exit-code: "1"
ignore-unfixed: true
vuln-type: os,library
severity: HIGH,CRITICAL
# Allow 5 minutes before timing out
timeout: 5m0s

- name: Run Trivy — SARIF upload (informational, all severities)
if: always()
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.30.0
with:
image-ref: vortex-backend:smoke-amd64
format: sarif
output: trivy-results.sarif
ignore-unfixed: false
vuln-type: os,library
severity: LOW,MEDIUM,HIGH,CRITICAL

- name: Upload Trivy SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
category: trivy-container

# ── amd64 smoke test ──────────────────────────────────────────────────────────
smoke-amd64:
name: Smoke test — linux/amd64
runs-on: ubuntu-latest
needs: build
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: vortex
POSTGRES_PASSWORD: vortex
POSTGRES_DB: vortex
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10

steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

- name: Download amd64 tarball
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: smoke-amd64
path: /tmp

- name: Load amd64 image
run: docker load < /tmp/smoke-amd64.tar.gz

- name: Run smoke test (amd64)
env:
ARCH: amd64
IMAGE: vortex-backend:smoke-amd64
DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
REDIS_URL: redis://localhost:6379
run: bash scripts/ci/container-smoke-test.sh

# ── arm64 smoke test ──────────────────────────────────────────────────────────
smoke-arm64:
name: Smoke test — linux/arm64 (QEMU)
runs-on: ubuntu-latest
needs: build
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: vortex
POSTGRES_PASSWORD: vortex
POSTGRES_DB: vortex
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10

steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

- name: Set up QEMU (arm64 emulation)
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff9c9f7eb5ca85ccdb5 # v3.6.0
with:
platforms: linux/arm64

- name: Download arm64 tarball
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: smoke-arm64
path: /tmp

- name: Load arm64 image
run: docker load < /tmp/smoke-arm64.tar.gz

- name: Run smoke test (arm64)
env:
ARCH: arm64
IMAGE: vortex-backend:smoke-arm64
DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
REDIS_URL: redis://localhost:6379
run: bash scripts/ci/container-smoke-test.sh
Loading
Loading