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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
!/yarn.lock
!/src
!/fonts
!/smoketest.js
64 changes: 42 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,38 +1,58 @@
FROM node:24.14.0 AS builder
FROM us-docker.pkg.dev/sentryio/dhi/node:24-debian13-dev AS builder

COPY package.json yarn.lock .
WORKDIR /build

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

COPY tsconfig.json .
COPY src src
RUN yarn build

FROM node:24.14.0-slim
# Drop devDependencies from node_modules for the runtime image
RUN yarn install --frozen-lockfile --production

# canvas 3.x bundles its graphics libs (libcairo, libpango, etc.) but some of
# those bundled libs still need system libs absent from the minimal runtime image.
# Use ldd to auto-detect all transitive system dependencies of every native module
# so this stays correct when canvas or any other native dependency is updated.
RUN mkdir -p /canvas-sys-libs && \
find /build/node_modules -name "*.node" | \
xargs ldd 2>/dev/null | \
awk '/=> \// { print $3 }' | \
grep -v '^/build' | \
sort -u | \
while IFS= read -r lib; do \
real=$(readlink -f "$lib"); \
cp --parents "$real" /canvas-sys-libs/ 2>/dev/null || true; \
usr="${lib/#\/lib\//\/usr\/lib\/}"; \
[ "$usr" != "$real" ] && \
ln -sf "$(basename "$real")" "/canvas-sys-libs$usr" 2>/dev/null || true; \
done

# canvas bundles libfontconfig but needs /etc/fonts/fonts.conf to initialise.
# Without it fontconfig silently fails and all text renders as box glyphs.
RUN apt-get update -qq && \
apt-get install -qq -y --no-install-recommends fontconfig && \
rm -rf /var/lib/apt/lists/*


FROM us-docker.pkg.dev/sentryio/dhi/node:24-debian13

ENV NODE_ENV=production

RUN npm install -g npm@latest \
&& npm cache clean --force

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile \
&& yarn cache clean

COPY package.json ./
COPY fonts fonts
COPY --from=builder lib lib

RUN node lib/index.js --help
COPY --from=builder /build/node_modules node_modules
COPY --from=builder /build/lib lib
COPY --from=builder /canvas-sys-libs/usr/lib/ /usr/lib/
COPY --from=builder /etc/fonts/ /etc/fonts/
COPY --from=builder /usr/share/fonts/ /usr/share/fonts/
COPY smoketest.js .

RUN ["node", "smoketest.js"]

EXPOSE 9090/tcp
CMD ["node", "./lib/index.js", "server", "9090"]
2 changes: 1 addition & 1 deletion devservices/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ services:
labels:
- orchestrator=devservices
healthcheck:
test: python3 -c "import urllib.request; urllib.request.urlopen(\"http://127.0.0.1:9090/api/chartcuterie/healthcheck/live\", timeout=5)"
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:9090/api/chartcuterie/healthcheck/live').then(r=>r.ok?0:1,()=>1).then(process.exit)"]
interval: 5s
timeout: 5s
retries: 3
Expand Down
38 changes: 38 additions & 0 deletions smoketest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';
// Exercises the full rendering path: canvas init, font loading, echarts render, PNG output.
// Runs as a build-time check in the Docker runtime stage — any missing .so or font files
// will cause this to throw and fail the build.
//
// The expected hash pins the exact pixel output. If it changes, update it by running:
// docker build -t chartcuterie:local . && \
// docker run --rm chartcuterie:local node -e "
// const crypto = require('crypto');
// const {renderSync} = require('./lib/render');
// const r = renderSync({key:'smoke',height:100,width:100,getOption:()=>({
// xAxis:{type:'category'},yAxis:{type:'value'},series:[{type:'line',data:[1,2,3]}]
// })},[]);
// console.log(crypto.createHash('sha256').update(r.buffer).digest('hex'));
// r.dispose();
// "
const crypto = require('crypto');
const {renderSync} = require('./lib/render');

const result = renderSync({
key: 'smoke',
height: 100,
width: 100,
getOption: () => ({
xAxis: {type: 'category'},
yAxis: {type: 'value'},
series: [{type: 'line', data: [1, 2, 3]}],
}),
}, []);

const {buffer} = result;
result.dispose();

const EXPECTED_SHA256 = '2616947b26199985b53250044725868a530ee2e2328ed2380825a2cbd71c37f9';
const actual = crypto.createHash('sha256').update(buffer).digest('hex');
if (actual !== EXPECTED_SHA256) {
throw new Error(`smoke test failed: output hash ${actual} !== expected ${EXPECTED_SHA256}`);
}