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
29 changes: 26 additions & 3 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ concurrency:
cancel-in-progress: false

permissions:
contents: write
packages: write
contents: write # Create the GitHub Release
packages: write # Push to GHCR
id-token: write # OIDC for actions/attest-build-provenance
attestations: write # Persist the build provenance attestation

jobs:
ci:
Expand Down Expand Up @@ -70,6 +72,7 @@ jobs:
uses: docker/setup-buildx-action@v4

- name: Build and push
id: build
uses: docker/build-push-action@v7
with:
context: .
Expand All @@ -79,6 +82,23 @@ jobs:
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true

- name: Generate SBOM (CycloneDX)
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cdx.json
upload-artifact: true

- name: Attest build provenance
uses: actions/attest-build-provenance@v3
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

- name: Preflight — verify ~/.env.app on VPS
uses: appleboy/ssh-action@v1
Expand Down Expand Up @@ -117,7 +137,10 @@ jobs:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1
env:
IMAGE: ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
# Pin by digest, not tag — guarantees the VPS pulls exactly what we just
# built. A tag-based deploy can race a second concurrent build that
# rewrites :latest / :<version> mid-rollout.
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
PORT: ${{ secrets.APP_PORT || '3000' }}
with:
host: ${{ secrets.VPS_HOST }}
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Example: Node.js application
# Replace with your own Dockerfile for other languages
# See docs/DOCKERFILE_EXAMPLES.md for Python, Go, and more
#
# Pinned to a digest for reproducibility. Dependabot's docker ecosystem
# refreshes this on a schedule. Node 20 reached EOL on 2026-04-30, so
# this targets Node 22 (active LTS through 2027-04).

FROM node:20-alpine
FROM node:22-alpine@sha256:cb15fca92530d7ac113467696cf1001208dac49c3c64355fd1348c11a88ddf8f

WORKDIR /app
COPY --chown=node:node app/ .

# Run as the non-root `node` user that ships with the official image.
USER node

EXPOSE 3000
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ services:
# Uncomment for apps with dependencies (node_modules, vendor, venv, etc.):
# - /app/node_modules
restart: unless-stopped
# Hardening defaults — override per workload if your app actually needs the
# capability (e.g. binding to ports <1024 → keep `cap_add: [NET_BIND_SERVICE]`).
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:${PORT:-3000}/health || exit 1"]
interval: 10s
Expand Down
Loading