Skip to content

Commit 58858b8

Browse files
authored
feat: enhance Docker build process with cache busting and OpenSSL verification (#48)
1 parent d235f83 commit 58858b8

3 files changed

Lines changed: 41 additions & 6 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ jobs:
233233
# Phase 1: Build into local Docker daemon for scanning.
234234
# EXACT same parameters as pr.yml production-simulation:
235235
# target: production, build-args: NODE_ENV=production, GHA cache.
236-
# The shared GHA cache means layers built in PR are reused here.
236+
# CACHE_BUSTER forces rebuild when package-lock.json changes (prevents stale deps).
237+
# Cache scoped to production to prevent cross-branch contamination from PR builds.
237238
- name: Build Docker image (pre-scan, no push)
238239
uses: docker/build-push-action@v6
239240
with:
@@ -242,17 +243,30 @@ jobs:
242243
target: production
243244
build-args: |
244245
NODE_ENV=production
246+
CACHE_BUSTER=${{ hashFiles('**/package-lock.json') }}
245247
push: false
246248
load: true
247249
tags: |
248250
fieldtrack-backend:${{ steps.meta.outputs.sha_short }}
249-
fieldtrack-backend:latest
250-
cache-from: type=gha
251-
cache-to: type=gha,mode=max
251+
cache-from: type=gha,scope=production
252+
cache-to: type=gha,mode=max,scope=production
253+
254+
# Verify OpenSSL version in PRODUCTION stage (not builder or runtime-deps).
255+
# Confirms dependencies were rebuilt AND are present in final distroless image.
256+
- name: Verify OpenSSL in production image
257+
run: |
258+
IMAGE_NAME="fieldtrack-backend:${{ steps.meta.outputs.sha_short }}"
259+
# Run against production image (distroless) — would fail if deps layer missed rebuild
260+
OPENSSL_VERSION=$(docker run --rm "$IMAGE_NAME" openssl version 2>&1)
261+
if [ $? -ne 0 ] || [ -z "$OPENSSL_VERSION" ]; then
262+
echo "::error::OpenSSL check failed — dependencies were not rebuilt or not copied to production stage"
263+
echo "Output: $OPENSSL_VERSION"
264+
exit 1
265+
fi
266+
echo "✓ Production image verified: $OPENSSL_VERSION"
252267
253268
# Capture the content-addressable image digest.
254-
# When source + cache + build-args are identical between PR and deploy,
255-
# this digest will match the one stored in the PR simulation artifact.
269+
# With cache scoping and cache busting, digest should always reproduce correctly.
256270
- name: Capture image digest
257271
id: digest
258272
run: |

.github/workflows/pr.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ jobs:
9696
run: |
9797
docker build \
9898
--target production \
99+
--build-arg CACHE_BUSTER=${{ hashFiles('**/package-lock.json') }} \
100+
--cache-from=type=gha,scope=pr \
101+
--cache-to=type=gha,mode=max,scope=pr \
99102
-t fieldtrack-backend:ci-validation \
100103
-f apps/api/Dockerfile \
101104
.

apps/api/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@
1515

1616
# ---- Stage 1: Build --------------------------------------------------------
1717
# Pinned to specific version to prevent supply chain attacks.
18+
# NOTE: To pin by digest and prevent base image drift:
19+
# 1. Run: docker pull node:24.2.0-bookworm-slim
20+
# 2. Run: docker inspect node:24.2.0-bookworm-slim | grep RepoDigests
21+
# 3. Replace node:24.2.0-bookworm-slim with node:24.2.0-bookworm-slim@sha256:DIGEST
22+
# This ensures identical binaries even if the tag is re-released.
1823
FROM node:24.2.0-bookworm-slim AS builder
1924

25+
# Cache buster: force rebuild when package-lock.json changes.
26+
# Prevents stale dependency layers from being reused on deployment.
27+
ARG CACHE_BUSTER=1
28+
2029
WORKDIR /workspace
2130

2231
# Copy package manifests first for layer-cached dependency install.
@@ -41,8 +50,12 @@ RUN npm run build
4150
# Separate stage: installs --omit=dev so distroless never needs npm or a shell.
4251
# mkdir -p guards ensure workspace subdirectories always exist for the COPY in
4352
# stage 3, even when npm hoists all deps to the root node_modules.
53+
# NOTE: Must use SAME base image tag as Stage 1 to ensure consistency.
4454
FROM node:24.2.0-bookworm-slim AS runtime-deps
4555

56+
# Cache buster: force rebuild when package-lock.json changes.
57+
ARG CACHE_BUSTER=1
58+
4659
WORKDIR /workspace
4760

4861
COPY package.json package-lock.json ./
@@ -63,6 +76,11 @@ RUN npm ci \
6376
# • Minimal glibc + libssl from Debian 12
6477
# • No shell, no package manager, no OS utilities
6578
# Trivy finds near-zero OS CVEs in this image.
79+
# NOTE: To pin by digest and prevent base image drift:
80+
# 1. Run: docker pull gcr.io/distroless/nodejs24-debian12:nonroot
81+
# 2. Run: docker inspect gcr.io/distroless/nodejs24-debian12:nonroot | grep RepoDigests
82+
# 3. Replace tag with @sha256:DIGEST in FROM statement
83+
# This prevents "same Dockerfile → different result" risks.
6684
# ENTRYPOINT is ["/nodejs/bin/node"]; CMD supplies the script path argument.
6785
FROM gcr.io/distroless/nodejs24-debian12:nonroot AS production
6886

0 commit comments

Comments
 (0)