├── .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 | node-docker 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 | --------------------------------------------------------------------------------