Skip to content

Commit 2777e63

Browse files
committed
feat(node): add -slim image variants without preinstalled SDKs
Every node image release now also builds and pushes a -slim variant of each tag (e.g. apify/actor-node:24-slim, apify/actor-node-playwright-chrome:24-1.60.0-slim). Each image dir gains a package.slim.json — the same manifest minus apify, crawlee and typescript (and the crawlee overrides), keeping only the browser library the image is built around. Slim images are built from the same Dockerfiles with SLIM=1, which installs from package.slim.json instead of package.json; regular tags are unchanged. set-dependency-versions.js keeps both manifests' versions in sync during releases. Actors on -slim get exactly the dependency versions in their own package.json instead of whatever the image preinstalled, at the cost of installing them at build time. The default main.js smoke tests no longer depend on apify or crawlee (which -slim images don't ship): apify is imported optionally, and the browsers are driven with raw playwright/puppeteer instead of crawlee's launchers, so one test suite serves both variants.
1 parent 189790f commit 2777e63

32 files changed

Lines changed: 552 additions & 183 deletions

.github/scripts/prepare-node-image-tags.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module.exports = () => {
2-
const { CURRENT_NODE, LATEST_NODE, RELEASE_TAG, IMAGE_NAME, FRAMEWORK_VERSION, IS_LATEST_BROWSER_IMAGE } = process.env
2+
const { CURRENT_NODE, LATEST_NODE, RELEASE_TAG, IMAGE_NAME, FRAMEWORK_VERSION, IS_LATEST_BROWSER_IMAGE, TAG_SUFFIX } = process.env
33
const tags = [];
4+
// Optional suffix appended to every tag (e.g. "-slim" -> apify/actor-node:20-slim).
5+
const suffix = TAG_SUFFIX || '';
46

57
if (CURRENT_NODE === LATEST_NODE && IS_LATEST_BROWSER_IMAGE === 'true') {
68
// apify/actor-node-x:latest
@@ -31,5 +33,7 @@ module.exports = () => {
3133
tags.push(`${IMAGE_NAME}:${CURRENT_NODE}-${RELEASE_TAG}`);
3234
}
3335

34-
return { allTags: tags.join(','), firstImageName: tags[0] };
36+
const suffixedTags = tags.map((tag) => `${tag}${suffix}`);
37+
38+
return { allTags: suffixedTags.join(','), firstImageName: suffixedTags[0] };
3539
}

.github/scripts/set-dependency-versions.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const fs = require('fs');
22

3-
const PACKAGE_JSON_PATH = './package.json';
3+
// package.slim.json is the manifest for the -slim image variants; keep it in sync.
4+
const PACKAGE_JSON_PATHS = ['./package.json', './package.slim.json'];
45

56
const DEPENDENCY_VERSIONS = {
67
'apify': process.env.APIFY_VERSION,
@@ -13,18 +14,21 @@ const DEPENDENCY_VERSIONS = {
1314
'camoufox-js': process.env.CAMOUFOX_VERSION,
1415
};
1516

16-
const pkg = readPackageJson(PACKAGE_JSON_PATH);
17-
updateDependencyVersions(pkg, DEPENDENCY_VERSIONS);
18-
fs.writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 4));
17+
for (const path of PACKAGE_JSON_PATHS) {
18+
const pkg = readPackageJson(path);
19+
if (!pkg) continue;
20+
updateDependencyVersions(pkg, DEPENDENCY_VERSIONS);
21+
fs.writeFileSync(path, JSON.stringify(pkg, null, 4));
22+
}
1923

2024
function readPackageJson(path) {
2125
try {
2226
const pkgJson = fs.readFileSync(path, 'utf-8');
2327
return JSON.parse(pkgJson);
2428
} catch (err) {
2529
if (err.code === 'ENOENT') {
26-
console.log(`No package.json to update in ${process.cwd()}`);
27-
process.exit(0);
30+
console.log(`No ${path} to update in ${process.cwd()}`);
31+
return undefined;
2832
}
2933
}
3034
}

.github/workflows/release-node-playwright.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,22 @@ jobs:
203203
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
204204
return generateTags();
205205
206+
- name: Prepare slim image tags
207+
id: prepare-slim-tags
208+
uses: actions/github-script@v8
209+
env:
210+
CURRENT_NODE: ${{ matrix.node-version }}
211+
LATEST_NODE: ${{ matrix.latest-node-version }}
212+
FRAMEWORK_VERSION: ${{ matrix.playwright-version }}
213+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
214+
IMAGE_NAME: apify/actor-${{ matrix.image-name }}
215+
IS_LATEST_BROWSER_IMAGE: ${{ matrix.is-latest }}
216+
TAG_SUFFIX: "-slim"
217+
with:
218+
script: |
219+
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
220+
return generateTags();
221+
206222
- name: Set up QEMU
207223
uses: docker/setup-qemu-action@v3
208224

@@ -240,6 +256,28 @@ jobs:
240256
yarn cache dir | grep -q "^/pkg-cache/yarn"
241257
'
242258
259+
- name: Build and tag slim image for testing
260+
uses: docker/build-push-action@v6
261+
with:
262+
context: ./${{ matrix.image-name }}
263+
file: ./${{ matrix.image-name }}/Dockerfile
264+
# PLAYWRIGHT_VERSION here is used for the full node-playwright image
265+
build-args: |
266+
NODE_VERSION=${{ matrix.node-version }}
267+
PLAYWRIGHT_VERSION=${{ format('v{0}-', matrix.playwright-version) }}
268+
SLIM=1
269+
platforms: linux/amd64
270+
provenance: false
271+
load: true
272+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
273+
cache-from: |
274+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}-slim
275+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}
276+
cache-to: type=gha,mode=max,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}-slim
277+
278+
- name: Test slim image
279+
run: docker run ${{ fromJson(steps.prepare-slim-tags.outputs.result).firstImageName }}
280+
243281
- name: Login to DockerHub
244282
if: github.event_name != 'pull_request'
245283
uses: docker/login-action@v4
@@ -262,3 +300,21 @@ jobs:
262300
tags: ${{ fromJson(steps.prepare-tags.outputs.result).allTags }}
263301
outputs: type=image,oci-mediatypes=true
264302
cache-from: type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}
303+
- name: Build and push slim OCI image
304+
if: github.event_name != 'pull_request'
305+
uses: docker/build-push-action@v6
306+
with:
307+
context: ./${{ matrix.image-name }}
308+
file: ./${{ matrix.image-name }}/Dockerfile
309+
build-args: |
310+
NODE_VERSION=${{ matrix.node-version }}
311+
PLAYWRIGHT_VERSION=${{ format('v{0}-', matrix.playwright-version) }}
312+
SLIM=1
313+
platforms: ${{ matrix.supports-arm64 == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
314+
provenance: true
315+
push: true
316+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
317+
outputs: type=image,oci-mediatypes=true
318+
cache-from: |
319+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}-slim
320+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.playwright-version }}

.github/workflows/release-node-puppeteer.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,22 @@ jobs:
185185
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
186186
return generateTags();
187187
188+
- name: Prepare slim image tags
189+
id: prepare-slim-tags
190+
uses: actions/github-script@v8
191+
env:
192+
CURRENT_NODE: ${{ matrix.node-version }}
193+
LATEST_NODE: ${{ matrix.latest-node-version }}
194+
FRAMEWORK_VERSION: ${{ matrix.puppeteer-version }}
195+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
196+
IMAGE_NAME: apify/actor-${{ matrix.image-name }}
197+
IS_LATEST_BROWSER_IMAGE: ${{ matrix.is-latest }}
198+
TAG_SUFFIX: "-slim"
199+
with:
200+
script: |
201+
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
202+
return generateTags();
203+
188204
- name: Set up QEMU
189205
uses: docker/setup-qemu-action@v3
190206

@@ -221,6 +237,27 @@ jobs:
221237
yarn cache dir | grep -q "^/pkg-cache/yarn"
222238
'
223239
240+
- name: Build and tag slim image for testing
241+
uses: docker/build-push-action@v6
242+
with:
243+
context: ./${{ matrix.image-name }}
244+
file: ./${{ matrix.image-name }}/Dockerfile
245+
build-args: |
246+
NODE_VERSION=${{ matrix.node-version }}
247+
PUPPETEER_VERSION=${{ matrix.puppeteer-version }}
248+
SLIM=1
249+
platforms: linux/amd64
250+
provenance: false
251+
load: true
252+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
253+
cache-from: |
254+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}-slim
255+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}
256+
cache-to: type=gha,mode=max,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}-slim
257+
258+
- name: Test slim image
259+
run: docker run ${{ fromJson(steps.prepare-slim-tags.outputs.result).firstImageName }}
260+
224261
- name: Login to DockerHub
225262
if: github.event_name != 'pull_request'
226263
uses: docker/login-action@v4
@@ -243,3 +280,21 @@ jobs:
243280
tags: ${{ fromJson(steps.prepare-tags.outputs.result).allTags }}
244281
outputs: type=image,oci-mediatypes=true
245282
cache-from: type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}
283+
- name: Build and push slim OCI image
284+
if: github.event_name != 'pull_request'
285+
uses: docker/build-push-action@v6
286+
with:
287+
context: ./${{ matrix.image-name }}
288+
file: ./${{ matrix.image-name }}/Dockerfile
289+
build-args: |
290+
NODE_VERSION=${{ matrix.node-version }}
291+
PUPPETEER_VERSION=${{ matrix.puppeteer-version }}
292+
SLIM=1
293+
platforms: linux/amd64
294+
provenance: true
295+
push: true
296+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
297+
outputs: type=image,oci-mediatypes=true
298+
cache-from: |
299+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}-slim
300+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-${{ matrix.puppeteer-version }}

.github/workflows/release-node.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,22 @@ jobs:
179179
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
180180
return generateTags();
181181
182+
- name: Prepare slim image tags
183+
id: prepare-slim-tags
184+
uses: actions/github-script@v8
185+
env:
186+
CURRENT_NODE: ${{ matrix.node-version }}
187+
LATEST_NODE: ${{ matrix.latest-node-version }}
188+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
189+
IMAGE_NAME: apify/actor-${{ matrix.image-name }}
190+
# Force this to true, as these images have no browsers
191+
IS_LATEST_BROWSER_IMAGE: "true"
192+
TAG_SUFFIX: "-slim"
193+
with:
194+
script: |
195+
const generateTags = require("./.github/scripts/prepare-node-image-tags.js");
196+
return generateTags();
197+
182198
- name: Set up QEMU
183199
uses: docker/setup-qemu-action@v3
184200

@@ -213,6 +229,26 @@ jobs:
213229
yarn cache dir | grep -q "^/pkg-cache/yarn"
214230
'
215231
232+
- name: Build and tag slim image for testing
233+
uses: docker/build-push-action@v6
234+
with:
235+
context: ./${{ matrix.image-name }}
236+
file: ./${{ matrix.image-name }}/Dockerfile
237+
build-args: |
238+
NODE_VERSION=${{ matrix.node-version }}
239+
SLIM=1
240+
platforms: linux/amd64
241+
provenance: false
242+
load: true
243+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
244+
cache-from: |
245+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-slim
246+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}
247+
cache-to: type=gha,mode=max,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-slim
248+
249+
- name: Test slim image
250+
run: docker run ${{ fromJson(steps.prepare-slim-tags.outputs.result).firstImageName }}
251+
216252
- name: Login to DockerHub
217253
if: github.event_name != 'pull_request'
218254
uses: docker/login-action@v4
@@ -233,3 +269,20 @@ jobs:
233269
tags: ${{ fromJson(steps.prepare-tags.outputs.result).allTags }}
234270
outputs: type=image,oci-mediatypes=true
235271
cache-from: type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}
272+
- name: Build and push slim OCI image
273+
if: github.event_name != 'pull_request'
274+
uses: docker/build-push-action@v6
275+
with:
276+
context: ./${{ matrix.image-name }}
277+
file: ./${{ matrix.image-name }}/Dockerfile
278+
build-args: |
279+
NODE_VERSION=${{ matrix.node-version }}
280+
SLIM=1
281+
platforms: linux/amd64,linux/arm64
282+
provenance: true
283+
push: true
284+
tags: ${{ fromJson(steps.prepare-slim-tags.outputs.result).allTags }}
285+
outputs: type=image,oci-mediatypes=true
286+
cache-from: |
287+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}-slim
288+
type=gha,scope=${{ matrix.image-name }}-${{ matrix.node-version }}

node-playwright-camoufox/Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ USER myuser
107107
WORKDIR /home/myuser
108108

109109
# Copy source code and xvfb script
110-
COPY --chown=myuser:myuser package.json main.js check-playwright-version.mjs firefox_test.js start_xvfb_and_run_cmd.sh new_xvfb_run_cmd.sh xvfb-entrypoint.sh /home/myuser/
110+
COPY --chown=myuser:myuser package.json package.slim.json main.js check-playwright-version.mjs firefox_test.js start_xvfb_and_run_cmd.sh new_xvfb_run_cmd.sh xvfb-entrypoint.sh /home/myuser/
111111

112112
# Tell Node.js this is a production environment
113113
ENV NODE_ENV=production
@@ -138,6 +138,12 @@ ENV NPM_CONFIG_CACHE=/pkg-cache/npm \
138138
ENV PNPM_CONFIG_NODE_LINKER=hoisted \
139139
YARN_NODE_LINKER=node-modules
140140

141+
# The -slim variant ships without the preinstalled Actor SDKs (apify, crawlee, typescript):
142+
# it installs from package.slim.json instead, which keeps only the browser library the image
143+
# is built around.
144+
ARG SLIM=0
145+
RUN if [ "$SLIM" = "1" ]; then mv package.slim.json package.json; else rm package.slim.json; fi
146+
141147
# Install default dependencies, print versions of everything
142148
RUN npm --quiet set progress=false \
143149
\
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1-
const { launchPlaywright } = require('crawlee');
1+
const playwright = require('playwright');
22
const { launchOptions: camoufoxLaunchOptions } = require('camoufox-js');
33

44
const testPageLoading = async (browser) => {
55
const page = await browser.newPage();
66
await page.goto('http://www.example.com');
77
const pageTitle = await page.title();
88
if (pageTitle !== 'Example Domain') {
9-
throw new Error(`Playwright+Firefox test failed - returned title "${pageTitle}"" !== "Example Domain"`);
9+
throw new Error(`Playwright+Firefox test failed - returned title "${pageTitle}" !== "Example Domain"`);
1010
}
1111
};
1212

13-
const testFirefox = async (launchOptions) => {
14-
const launchContext = {
15-
launcher: require('playwright').firefox,
16-
launchOptions: await camoufoxLaunchOptions({
17-
...launchOptions,
18-
}),
19-
};
13+
const testFirefox = async (launchOptions = {}) => {
14+
const options = await camoufoxLaunchOptions({ ...launchOptions });
2015

21-
console.log(`Testing Playwright with Firefox`, launchContext.launchOptions);
16+
console.log('Testing Playwright with Camoufox', options);
2217

23-
const browser = await launchPlaywright(launchContext);
18+
const browser = await playwright.firefox.launch(options);
2419

25-
await testPageLoading(browser);
26-
await browser.close();
20+
try {
21+
await testPageLoading(browser);
22+
} finally {
23+
await browser.close();
24+
}
2725
};
2826

2927
module.exports = testFirefox;

node-playwright-camoufox/main.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,31 @@ For more information, see https://docs.apify.com/actors/development/source-code#
1111
`);
1212
console.log('Testing Docker image...');
1313

14-
const { Actor } = require('apify');
15-
const { getMemoryInfo } = require('crawlee');
16-
const testFirefox = require('./firefox_test');
14+
// `apify` is optional: the -slim images ship without it preinstalled.
15+
let Actor;
16+
try {
17+
({ Actor } = require('apify'));
18+
} catch {
19+
Actor = undefined;
20+
}
1721

18-
Actor.main(async () => {
19-
// Sanity test browsers.
22+
const testFirefox = require('./firefox_test');
2023

21-
// Try to use full Firefox headless
24+
const run = async () => {
25+
// Camoufox (Firefox) headless
2226
await testFirefox({ headless: true });
2327

24-
// Try to use full Firefox with XVFB
28+
// Camoufox (Firefox) with XVFB (headful)
2529
await testFirefox({ headless: false });
2630

27-
// Try to use playwright default
28-
await testFirefox({ executablePath: undefined });
29-
await testFirefox({ executablePath: process.env.APIFY_DEFAULT_BROWSER_PATH });
30-
31-
// Test that "ps" command is available, sometimes it was missing in official Node builds
32-
await getMemoryInfo();
33-
});
31+
console.log('... test PASSED');
32+
};
33+
34+
if (Actor) {
35+
Actor.main(run);
36+
} else {
37+
run().catch((error) => {
38+
console.error(error);
39+
process.exitCode = 1;
40+
});
41+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"description": "Anonymous actor on the Apify platform (with Firefox)",
3+
"version": "0.0.1",
4+
"license": "UNLICENSED",
5+
"main": "main.js",
6+
"scripts": {
7+
"start": "node main.js"
8+
},
9+
"dependencies": {
10+
"camoufox-js": "CAMOUFOX_VERSION",
11+
"impit": "latest",
12+
"playwright": "PLAYWRIGHT_VERSION"
13+
},
14+
"repository": {}
15+
}

0 commit comments

Comments
 (0)