├── .dockerignore
├── .editorconfig
├── .github
├── dependabot.yml
├── renovate.json
├── scripts
│ ├── generate-major-node-matrix.js
│ ├── generate-minor-node-matrix.js
│ ├── ignorelist.json
│ └── utils.js
└── workflows
│ ├── build-major.yml
│ ├── build-minor.yml
│ ├── documentation.yml
│ └── tests.yml
├── .gitignore
├── Dockerfile
├── LICENSE
└── README.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | .gitignore
4 | .github
5 | .editorconfig
6 | .idea
7 | .git
8 | *.md
9 | LICENSE
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # docs:
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
13 | [Dockerfile]
14 | indent_size = 4
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Docs:
2 |
3 | version: 2
4 |
5 | updates:
6 | - package-ecosystem: github-actions
7 | directory: /
8 | schedule: {interval: monthly}
9 | reviewers: [tarampampam]
10 | assignees: [tarampampam]
11 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>tarampampam/.github//renovate/default",
5 | ":rebaseStalePrs"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.github/scripts/generate-major-node-matrix.js:
--------------------------------------------------------------------------------
1 | const {fetchImageTagInfo, getEnv, shouldBeIgnored} = require('./utils')
2 |
3 | /**
4 | * @param {Object} github Docs:
5 | * @param {Object} context Docs:
6 | * @param {Object} core Docs:
7 | * @return {Promise<{include: Object[]}>}
8 | */
9 | module.exports = async ({github, context, core}) => {
10 | const env = {
11 | tagsList: getEnv('tags-list').split('\n').map(s => s.trim()).filter(s => {
12 | if (s.length === 0) { // skip empty lines
13 | return false
14 | }
15 |
16 | return !(s.startsWith('//') || s.startsWith('#')) // skip the commented lines
17 | }),
18 | sourceImage: getEnv('source-image'),
19 | targetImage: getEnv('target-image'),
20 | }
21 |
22 | if (env.tagsList.length === 0) {
23 | throw new Error('Empty tags list. Set required tag list using step "env.tags-list" key')
24 | } else if (env.sourceImage.length === 0) {
25 | throw new Error('Source image is not set. Set it using "env.source-image" key')
26 | } else if (env.targetImage.length === 0) {
27 | throw new Error('Target image is not set. Set it using "env.target-image" key')
28 | }
29 |
30 | core.info(`Tags list: ${env.tagsList.join(', ')}`)
31 | core.info(`Source image: ${env.sourceImage}, target image: ${env.targetImage}`)
32 |
33 | return await Promise.allSettled(env.tagsList.map(imageTag => {
34 | return new Promise((resolve, reject) => {
35 | fetchImageTagInfo(env.sourceImage, imageTag)
36 | .then(sourceImageInfo => {
37 | fetchImageTagInfo(env.targetImage, imageTag)
38 | .then(targetImageInfo => {
39 | const timeDeltaMillis = sourceImageInfo.pushedAt.getTime() - targetImageInfo.pushedAt.getTime()
40 |
41 | core.info(`Time difference between source and target images for the ${imageTag}: ${timeDeltaMillis / 1000} sec.`)
42 |
43 | if (targetImageInfo.pushedAt.getTime() < sourceImageInfo.pushedAt.getTime()) {
44 | core.info(`Plan to build an image with the tag ${imageTag} (time delta ${timeDeltaMillis / 1000} sec)`)
45 |
46 | return resolve(sourceImageInfo) // source image has a more recent update date - process it
47 | }
48 |
49 | reject(new Error(`The image tag ${imageTag} already updated - skip it`))
50 | })
51 | .catch(_ => {
52 | resolve(sourceImageInfo) // we have no this tag in the target repository, and therefore must process it
53 | })
54 | })
55 | .catch(reject)
56 | })
57 | })).then(async (promisesList) => {
58 | // matrix docs:
59 | /** @type {{include: {tag: string, platforms: string}[]}} */
60 | const matrix = {include: []}
61 |
62 | promisesList.forEach(promise => {
63 | if (promise.status === 'fulfilled') {
64 | /** @type {{tag: string, arch: string[]}} */
65 | const image = promise.value
66 |
67 | image.arch = image.arch.filter(arch => {
68 | const should = shouldBeIgnored(image.tag, arch)
69 |
70 | if (should === true) {
71 | core.info(`Architecture ${arch} for the tag ${image.tag} ignored (rule from the ignore-list)`)
72 | }
73 |
74 | return !should
75 | })
76 |
77 | if (image.arch.length !== 0) {
78 | matrix.include.push({
79 | tag: image.tag,
80 | platforms: image.arch.join(','),
81 | })
82 | } else {
83 | core.notice(`Tag ${image.tag} ignored (it does not contain the architectures)`)
84 | }
85 | } else {
86 | core.notice(promise.reason.message)
87 | }
88 | })
89 |
90 | if (matrix.include.length === 0) {
91 | core.warning('Nothing to do (empty matrix items)')
92 |
93 | // await github.rest.actions.cancelWorkflowRun({
94 | // owner: context.repo.owner,
95 | // repo: context.repo.repo,
96 | // run_id: context.runId,
97 | // })
98 | }
99 |
100 | if (matrix.include.length > 255) { // maximal matrix size is 256 jobs per run:
101 | core.notice(`Matrix size limited (was: ${matrix.include.length}, become: 255)`)
102 |
103 | matrix.include = matrix.include.slice(0, 255)
104 | }
105 |
106 | return matrix
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/.github/scripts/generate-minor-node-matrix.js:
--------------------------------------------------------------------------------
1 | const {fetchTagsHistory, getEnv, shouldBeIgnored} = require('./utils')
2 |
3 | /**
4 | * @param {Object} github Docs:
5 | * @param {Object} context Docs:
6 | * @param {Object} core Docs:
7 | * @return {Promise<{include: Object[]}>}
8 | */
9 | module.exports = async ({github, context, core}) => {
10 | const env = {
11 | sourceImage: getEnv('source-image'),
12 | targetImage: getEnv('target-image'),
13 | }
14 |
15 | if (env.sourceImage.length === 0) {
16 | throw new Error('Source image is not set. Set it using "env.source-image" key')
17 | } else if (env.targetImage.length === 0) {
18 | throw new Error('Target image is not set. Set it using "env.target-image" key')
19 | }
20 |
21 | core.info(`Source image: ${env.sourceImage}, target image: ${env.targetImage}`)
22 |
23 | /**
24 | * @param {{tag: string}} image
25 | * @return {boolean}
26 | */
27 | const tagsFilter = (image) => {
28 | return image.tag.endsWith('-alpine') // only alpine
29 | && /^\d+\.\d+[^.]+$/.test(image.tag) // only in MAJOR.MINOR format
30 | }
31 |
32 | const sourceTags = (await fetchTagsHistory(env.sourceImage, 30))
33 | .filter(tagsFilter) // the common filter
34 | .map(image => {
35 | image.arch = image.arch.filter(arch => {
36 | const should = shouldBeIgnored(image.tag, arch)
37 |
38 | if (should === true) {
39 | core.info(`Architecture ${arch} for the tag ${image.tag} ignored (rule from the ignore-list)`)
40 | }
41 |
42 | return !should
43 | })
44 |
45 | return image
46 | })
47 | .filter(image => {
48 | const ignore = image.arch.length === 0
49 |
50 | if (ignore) {
51 | core.notice(`Tag ${image.tag} ignored (it does not contain the architectures)`)
52 | }
53 |
54 | return !ignore
55 | })
56 |
57 | core.info(`${sourceTags.length} minor alpine-like tags were found for the ${env.sourceImage} image: ${sourceTags.map(i => i.tag).join(', ')}`)
58 |
59 | const targetTags = (await fetchTagsHistory(env.targetImage, 15)).filter(tagsFilter)
60 |
61 | core.info(`${targetTags.length} minor alpine-like tags were found for the ${env.targetImage} image: ${targetTags.map(i => i.tag).join(', ')}`)
62 |
63 | const diff = sourceTags.filter(sourceTag => {
64 | for (let i = 0; i < targetTags.length; i++) {
65 | if (targetTags[i].tag === sourceTag.tag) {
66 | if (targetTags[i].pushedAt.getTime() < sourceTag.pushedAt.getTime()) {
67 | core.info(`Plan to build the tag ${sourceTag.tag} due to updated timestamp (source tag is newer than target)`)
68 |
69 | return true
70 | }
71 |
72 | if (targetTags[i].arch.length !== sourceTag.arch.length) {
73 | core.info(`Plan to build the tag ${sourceTag.tag} due to new arch`)
74 |
75 | return true
76 | }
77 |
78 | core.notice(`The image tag ${sourceTag.tag} already exists and updated - skip it`)
79 |
80 | return false
81 | }
82 | }
83 |
84 | core.info(`Plan to build the tag ${sourceTag.tag}`)
85 |
86 | return true
87 | })
88 |
89 | /** @type {{include: {tag: string, platforms: string}[]}} */
90 | const matrix = {include: []}
91 |
92 | if (diff.length > 0) {
93 | core.notice(`Difference between ${env.targetImage} and ${env.sourceImage} tags is: ${diff.map(i => i.tag).join(', ')}`)
94 |
95 | matrix.include = diff.map(i => {
96 | return {tag: i.tag, platforms: i.arch.join(',')}
97 | })
98 | } else {
99 | core.warning('Nothing to do (difference was not found)')
100 | }
101 |
102 | if (matrix.include.length > 255) { // maximal matrix size is 256 jobs per run:
103 | core.notice(`Matrix size limited (was: ${matrix.include.length}, become: 255)`)
104 |
105 | matrix.include = matrix.include.slice(0, 255)
106 | }
107 |
108 | return matrix
109 | }
110 |
--------------------------------------------------------------------------------
/.github/scripts/ignorelist.json:
--------------------------------------------------------------------------------
1 | {
2 | "19-alpine": ["linux/arm/v6"],
3 | "19.0-alpine": ["linux/arm/v6"],
4 | "9.11-alpine": ["linux/386"],
5 | "9.10-alpine": ["linux/386"],
6 | "9.7-alpine": ["linux/386"],
7 | "9.6-alpine": ["linux/386"],
8 | "9.5-alpine": ["linux/386"],
9 | "9.4-alpine": ["linux/386"],
10 | "9.3-alpine": ["linux/386"],
11 | "9.2-alpine": ["linux/386"],
12 | "9.1-alpine": ["linux/386"],
13 | "9.0-alpine": ["linux/386"],
14 | "9-alpine": ["linux/386"],
15 | "8.17-alpine": ["linux/386"],
16 | "8.16-alpine": ["linux/386"],
17 | "8.15-alpine": ["linux/386"],
18 | "8.14-alpine": ["linux/386"],
19 | "8.13-alpine": ["linux/386"],
20 | "8.12-alpine": ["linux/386"],
21 | "8.11-alpine": ["linux/386"],
22 | "8.10-alpine": ["linux/386"],
23 | "8.9-alpine": ["linux/386"],
24 | "8-alpine": ["linux/386"],
25 | "7.5-alpine": ["*"],
26 | "7.4-alpine": ["*"],
27 | "7.3-alpine": ["*"],
28 | "7.2-alpine": ["*"],
29 | "6.17-alpine": ["linux/386"],
30 | "4.7-alpine": ["*"],
31 | "4.6-alpine": ["*"]
32 | }
33 |
--------------------------------------------------------------------------------
/.github/scripts/utils.js:
--------------------------------------------------------------------------------
1 | const https = require('https')
2 |
3 | /**
4 | * @param {string} uri
5 | * @param {number} wantStatusCode
6 | * @return {Promise}
7 | */
8 | const httpGet = (uri, wantStatusCode = 200) => {
9 | return new Promise((resolve, reject) => {
10 | const request = https.request(uri, {
11 | method: 'GET',
12 | timeout: 10 * 1000, // milliseconds
13 | }, (response) => {
14 | if (response.statusCode !== wantStatusCode) {
15 | response.resume()
16 |
17 | return reject(new Error(`wrong response code from ${uri}: ${response.statusCode} (expected code is ${wantStatusCode})`))
18 | }
19 |
20 | const buf = []
21 |
22 | response
23 | .on('data', chunk => buf.push(chunk))
24 | .on('end', () => {
25 | resolve(Buffer.concat(buf))
26 | })
27 | })
28 |
29 | request.on('error', reject)
30 |
31 | request.end()
32 | })
33 | }
34 |
35 | /**
36 | * @param {string} imageName
37 | * @param {string} imageTag
38 | * @return {Promise<{tag: string, arch: string[], pushedAt: Date}>}
39 | */
40 | const fetchImageTagInfo = (imageName, imageTag) => {
41 | return new Promise((resolve, reject) => {
42 | httpGet(`https://hub.docker.com/v2/repositories/${imageName}/tags/${imageTag}`) // default rate limit for the API calls is 600
43 | .then(buf => {
44 | /** @type {{
45 | * images: {architecture: ?string, variant: ?string, os: ?string}[],
46 | * last_updated: string,
47 | * name: string
48 | * }} */
49 | const payload = JSON.parse(buf.toString())
50 | const arch = []
51 |
52 | payload.images.forEach(image => {
53 | arch.push([image.os, image.architecture, image.variant].filter(s => {
54 | return typeof s === 'string' && s !== ''
55 | }).join('/'))
56 | })
57 |
58 | resolve({
59 | tag: payload.name,
60 | arch: arch,
61 | pushedAt: new Date(Date.parse(payload.last_updated)),
62 | })
63 | })
64 | .catch(reject)
65 | })
66 | }
67 |
68 | /**
69 | * @param {string} imageName
70 | * @param {number} pagesLimit
71 | * @return {Promise<{tag: string, arch: string[], pushedAt: Date}[]>}
72 | */
73 | const fetchTagsHistory = (imageName, pagesLimit = 5) => {
74 | return new Promise(async (resolve, reject) => {
75 | /** @type {{tag: string, arch: string[], pushedAt: Date}[]} */
76 | const tags = []
77 |
78 | for (let pageNumber = 1; pageNumber <= pagesLimit; pageNumber++) {
79 | let uri = `https://registry.hub.docker.com/v2/repositories/${imageName}/tags?page=${pageNumber}&page_size=100`
80 |
81 | const data = await httpGet(uri) // default rate limit for the Docker Hub API calls is 600
82 | /** @type {{
83 | * count: number,
84 | * next: ?string,
85 | * previous: ?string,
86 | * results: {images: {architecture: ?string, variant: ?string, os: ?string}[], last_updated: string, name: string}[]
87 | * }} */
88 | const payload = JSON.parse(data.toString())
89 |
90 | payload.results.forEach(result => {
91 | const arch = []
92 |
93 | result.images.forEach(image => {
94 | arch.push([image.os, image.architecture, image.variant].filter(s => {
95 | return typeof s === 'string' && s !== ''
96 | }).join('/'))
97 | })
98 |
99 | tags.push({
100 | tag: result.name,
101 | arch: arch,
102 | pushedAt: new Date(Date.parse(result.last_updated)),
103 | })
104 | })
105 |
106 | if (typeof payload.next === 'string') {
107 | uri = payload.next // change the initial uri with the "next page" uri
108 | } else {
109 | break
110 | }
111 | }
112 |
113 | resolve(tags)
114 | })
115 | }
116 |
117 | /**
118 | * @param {string} name
119 | * @return {string} empty string only if a variable was not set
120 | */
121 | const getEnv = (name) => {
122 | if (name in process.env) {
123 | const value = process.env[name]
124 |
125 | if (typeof value === 'string' && value.length > 0) {
126 | return value
127 | }
128 | }
129 |
130 | return ''
131 | }
132 |
133 | /**
134 | *
135 | * @type {Object.}
136 | */
137 | const tagsArchIgnoreList = require('./ignorelist.json')
138 |
139 | /**
140 | * @param {string} tag
141 | * @param {string} arch
142 | * @return {boolean}
143 | */
144 | const shouldBeIgnored = (tag, arch) => {
145 | if (tag in tagsArchIgnoreList) {
146 | const archList = tagsArchIgnoreList[tag]
147 |
148 | if (archList.length === 0) {
149 | return false
150 | }
151 |
152 | for (let i = 0; i < archList.length; i++) {
153 | if (archList[i] === '*' || archList[i] === arch) {
154 | return true
155 | }
156 | }
157 | }
158 |
159 | return false
160 | }
161 |
162 | module.exports = {
163 | fetchImageTagInfo,
164 | fetchTagsHistory,
165 | shouldBeIgnored,
166 | getEnv,
167 | }
168 |
--------------------------------------------------------------------------------
/.github/workflows/build-major.yml:
--------------------------------------------------------------------------------
1 | name: build-major
2 |
3 | on:
4 | schedule: [cron: '1 * * * *'] # every 1 hour # [cron: '10 0 */3 * *'] # every 3 days
5 | push:
6 | branches: [master, main]
7 | tags-ignore: ['**']
8 |
9 | jobs:
10 | plan:
11 | name: Generate the build matrix
12 | runs-on: ubuntu-20.04
13 | outputs:
14 | matrix: ${{ steps.set-matrix.outputs.result }}
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: actions/github-script@v6
19 | id: set-matrix
20 | with:
21 | github-token: 'empty' # the token is not needed for the script running
22 | script: return await require('./.github/scripts/generate-major-node-matrix.js')({github, context, core})
23 | env:
24 | source-image: library/node
25 | target-image: tarampampam/node
26 | tags-list: |
27 | latest
28 | alpine
29 | lts-alpine
30 | current-alpine
31 | 8-alpine
32 | 9-alpine
33 | 10-alpine
34 | 11-alpine
35 | 12-alpine
36 | 13-alpine
37 | 14-alpine
38 | 15-alpine
39 | 16-alpine
40 | 17-alpine
41 | 18-alpine
42 | 19-alpine
43 |
44 | build:
45 | name: Build the docker image (${{ matrix.tag }})
46 | needs: [plan]
47 | if: ${{ fromJSON(needs.plan.outputs.matrix).include[0] }} # docs:
48 | runs-on: ubuntu-20.04
49 | timeout-minutes: 10
50 | strategy:
51 | fail-fast: false
52 | matrix: ${{ fromJson(needs.plan.outputs.matrix) }}
53 | # include:
54 | # - {tag: foo, platforms: 'platform/one,platform/two/v2'}
55 | steps:
56 | - uses: actions/checkout@v3
57 |
58 | - uses: docker/setup-qemu-action@v2
59 |
60 | - uses: docker/setup-buildx-action@v2
61 |
62 | - uses: docker/login-action@v2
63 | with:
64 | username: ${{ secrets.DOCKER_LOGIN }}
65 | password: ${{ secrets.DOCKER_PASSWORD }}
66 |
67 | - uses: docker/login-action@v2
68 | with:
69 | registry: ghcr.io
70 | username: ${{ github.actor }}
71 | password: ${{ secrets.GITHUB_TOKEN }}
72 |
73 | - uses: docker/build-push-action@v3 # Action page:
74 | with:
75 | context: .
76 | file: Dockerfile
77 | push: true # comment this line for the local workflow running
78 | platforms: ${{ matrix.platforms }}
79 | build-args: "NODE_VERSION=${{ matrix.tag }}"
80 | tags: |
81 | tarampampam/node:${{ matrix.tag }}
82 | ghcr.io/${{ github.actor }}/node:${{ matrix.tag }}
83 |
--------------------------------------------------------------------------------
/.github/workflows/build-minor.yml:
--------------------------------------------------------------------------------
1 | name: build-minor
2 |
3 | on:
4 | schedule: [cron: '10 * * * *'] # every 1 hour
5 | push:
6 | branches: [master, main]
7 | tags-ignore: ['**']
8 |
9 | jobs:
10 | plan:
11 | name: Generate the build matrix
12 | runs-on: ubuntu-20.04
13 | outputs:
14 | matrix: ${{ steps.set-matrix.outputs.result }}
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: actions/github-script@v6
19 | id: set-matrix
20 | with:
21 | github-token: 'empty' # the token is not needed for the script running
22 | script: return await require('./.github/scripts/generate-minor-node-matrix.js')({github, context, core})
23 | env:
24 | source-image: library/node
25 | target-image: tarampampam/node
26 |
27 | build:
28 | name: Build the docker image (${{ matrix.tag }})
29 | needs: [plan]
30 | if: ${{ fromJSON(needs.plan.outputs.matrix).include[0] }} # docs:
31 | runs-on: ubuntu-20.04
32 | timeout-minutes: 10
33 | strategy:
34 | fail-fast: false
35 | matrix: ${{ fromJson(needs.plan.outputs.matrix) }}
36 | # include:
37 | # - {tag: foo, platforms: 'platform/one,platform/two/v2'}
38 | steps:
39 | - uses: actions/checkout@v3
40 |
41 | - uses: docker/setup-qemu-action@v2
42 |
43 | - uses: docker/setup-buildx-action@v2
44 |
45 | - uses: docker/login-action@v2
46 | with:
47 | username: ${{ secrets.DOCKER_LOGIN }}
48 | password: ${{ secrets.DOCKER_PASSWORD }}
49 |
50 | - uses: docker/login-action@v2
51 | with:
52 | registry: ghcr.io
53 | username: ${{ github.actor }}
54 | password: ${{ secrets.GITHUB_TOKEN }}
55 |
56 | - uses: docker/build-push-action@v3 # Action page:
57 | with:
58 | context: .
59 | file: Dockerfile
60 | push: true # comment this line for the local workflow running
61 | platforms: ${{ matrix.platforms }}
62 | build-args: "NODE_VERSION=${{ matrix.tag }}"
63 | tags: |
64 | tarampampam/node:${{ matrix.tag }}
65 | ghcr.io/${{ github.actor }}/node:${{ matrix.tag }}
66 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: documentation
2 |
3 | on:
4 | push:
5 | branches: [master, main]
6 | paths: ['README.md']
7 |
8 | jobs:
9 | docker-hub-description:
10 | name: Docker Hub Description
11 | runs-on: ubuntu-20.04
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - uses: peter-evans/dockerhub-description@v3 # Action page:
16 | with:
17 | username: ${{ secrets.DOCKER_LOGIN }}
18 | password: ${{ secrets.DOCKER_USER_PASSWORD }}
19 | repository: tarampampam/node
20 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on:
4 | push:
5 | branches: [master, main]
6 | tags-ignore: ['**']
7 | paths-ignore: ['**.md']
8 | pull_request:
9 | paths-ignore: ['**.md']
10 |
11 | concurrency:
12 | group: ${{ github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs: # Docs:
16 | gitleaks:
17 | name: Gitleaks
18 | runs-on: ubuntu-20.04
19 | steps:
20 | - uses: actions/checkout@v3
21 | with: {fetch-depth: 0}
22 |
23 | - name: Check for GitLeaks
24 | uses: gacts/gitleaks@v1 # Action page:
25 |
26 | build:
27 | name: Build the docker image
28 | runs-on: ubuntu-20.04
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | node-version: ['', alpine, latest, 6.17-alpine, 8.12-alpine]
33 | steps:
34 | - uses: actions/checkout@v3
35 |
36 | - uses: docker/setup-qemu-action@v2
37 |
38 | - uses: docker/setup-buildx-action@v2
39 |
40 | - uses: docker/build-push-action@v3 # Action page:
41 | with:
42 | context: .
43 | platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
44 | file: Dockerfile
45 | push: false
46 | build-args: "NODE_VERSION=${{ matrix.node-version }}"
47 | tags: node:ci
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEs
2 | /.vscode
3 | /.idea
4 |
5 | # Temp dirs & trash
6 | .DS_Store
7 | /temp
8 | /tmp
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1.2
2 |
3 | # e.g.: `docker build --rm --build-arg "NODE_VERSION=latest" -f ./Dockerfile .`
4 | # e.g.: `docker build --rm --build-arg "NODE_VERSION=11.8-alpine" -f ./Dockerfile .`
5 | ARG NODE_VERSION
6 |
7 | FROM node:${NODE_VERSION:-alpine}
8 |
9 | RUN set -x \
10 | && . /etc/os-release \
11 | && case "$ID" in \
12 | alpine) \
13 | apk add --no-cache bash git openssh \
14 | ;; \
15 | debian) \
16 | apt-get update \
17 | && apt-get -yq install bash git openssh-server \
18 | && apt-get -yq clean \
19 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
20 | ;; \
21 | esac \
22 | # install yarn, if needed (only applies to older versions, like 6 or 7)
23 | && yarn bin || ( npm install --global yarn && npm cache clean ) \
24 | # show installed application versions
25 | && git --version && bash --version && ssh -V && npm -v && node -v && yarn -v
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2016 tarampampam
2 |
3 | Everyone is permitted to copy and distribute verbatim or modified copies of this license
4 | document, and changing it is allowed as long as the name is changed.
5 |
6 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING,
7 | DISTRIBUTION AND MODIFICATION
8 |
9 | 0. You just DO WHAT THE FUCK YOU WANT TO.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | # Why?
13 |
14 | Base [`node`][base-node-image] image does not contain installed `git`, for example ([issue][node-586]). Because of this previously I had to build a separate image (installing many npm dependencies was otherwise impossible), but now we can just use this image :)
15 |
16 | ## Installed applications
17 |
18 | We had installed to the alpine-based images the following applications (using a package manager):
19 |
20 | - `git`
21 | - `bash`
22 | - `openssh`
23 |
24 | > If you think something else should be installed additionally, please create an [issue in this repository][new-issue] describing the reason
25 |
26 | ### What about updates?
27 |
28 | I took care of this - using periodic runs of GitHub actions tags in `major(.minor)-alpine` format are automatically rebuilt (if they have been updated). You can check all existing tags in one of the following docker-registries:
29 |
30 | | Registry | Image |
31 | |--------------------------------------------|----------------------------|
32 | | [Docker Hub][docker-hub] | `tarampampam/node` |
33 | | [GitHub Container Registry][ghcr] (mirror) | `ghcr.io/tarampampam/node` |
34 |
35 | All tags support architectures that are available in the original tags:
36 |
37 | ```bash
38 | $ docker run --rm mplatform/mquery tarampampam/node:latest
39 | Image: tarampampam/node:latest
40 | * Manifest List: Yes
41 | * Supported platforms:
42 | - linux/s390x
43 | - linux/ppc64le
44 | - linux/amd64
45 | - linux/arm64
46 | - linux/arm/v7
47 | ```
48 |
49 | ## Supported tags
50 |
51 | - `latest`
52 | - `alpine`
53 | - `lts-alpine`
54 | - `current-alpine`
55 | - `8-alpine`, `8.x-alpine` (deprecated)
56 | - `9-alpine`, `9.x-alpine` (deprecated)
57 | - `10-alpine`, `10.x-alpine`
58 | - `11-alpine`, `11.x-alpine`
59 | - `12-alpine`, `12.x-alpine`
60 | - `13-alpine`, `13.x-alpine`
61 | - `14-alpine`, `14.x-alpine`
62 | - `15-alpine`, `15.x-alpine`
63 | - `16-alpine`, `16.x-alpine`
64 | - `17-alpine`, `17.x-alpine`
65 | - `18-alpine`, `18.x-alpine`
66 | - `19-alpine`, `19.x-alpine`
67 |
68 | > Note: Some tags/platforms [are ignored](.github/scripts/ignorelist.json) due to the "Segmentation fault" errors
69 |
70 | ## How can I use this?
71 |
72 | For example:
73 |
74 | ```bash
75 | $ docker run --rm \
76 | --volume "$(pwd):/app" \
77 | --workdir "/app" \
78 | --user "$(id -u):$(id -g)" \
79 | tarampampam/node:17-alpine \
80 | yarn install
81 | ```
82 |
83 | Or using with `docker-compose.yml`:
84 |
85 | ```yml
86 | services:
87 | node:
88 | image: tarampampam/node:17-alpine
89 | volumes:
90 | - ./src:/app:rw
91 | working_dir: /app
92 | command: []
93 | ```
94 |
95 | ## License
96 |
97 | WTFPL. Use anywhere for your pleasure.
98 |
99 | [node-586]:https://github.com/nodejs/docker-node/issues/586
100 | [base-node-image]:https://hub.docker.com/_/node?tab=tags
101 | [docker-hub]:https://hub.docker.com/r/tarampampam/node/
102 | [ghcr]:https://github.com/tarampampam/node-docker/pkgs/container/node
103 | [new-issue]:https://github.com/tarampampam/node-docker/issues/new
104 |
--------------------------------------------------------------------------------