init
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
# default
|
||||
.git*
|
||||
maintain/
|
||||
LICENSE
|
||||
*.md
|
||||
img/
|
||||
node_modules/
|
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
# default
|
||||
* text=auto
|
||||
*.sh eol=lf
|
115
.github/workflows/cron.update.yml
vendored
Normal file
115
.github/workflows/cron.update.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: cron-update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
jobs:
|
||||
cron-update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: init / checkout
|
||||
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
with:
|
||||
ref: 'master'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: cron-update / get latest version
|
||||
run: |
|
||||
echo "LATEST_VERSION=$(curl -s https://api.github.com/repos/netbirdio/netbird/releases/latest | jq -r '.tag_name' | sed 's/v//')" >> "${GITHUB_ENV}"
|
||||
echo "LATEST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1` | sed 's/v//')" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: cron-update / setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm i semver
|
||||
|
||||
- name: cron-update / compare latest with current version
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { existsSync, readFileSync, writeFileSync } = require('node:fs');
|
||||
const { resolve } = require('node:path');
|
||||
const { inspect } = require('node:util');
|
||||
const semver = require('semver')
|
||||
const repository = {dot:{}};
|
||||
|
||||
try{
|
||||
const path = resolve('.json');
|
||||
if(existsSync(path)){
|
||||
try{
|
||||
repository.dot = JSON.parse(readFileSync(path).toString());
|
||||
}catch(e){
|
||||
throw new Error('could not parse .json');
|
||||
}
|
||||
}else{
|
||||
throw new Error('.json does not exist');
|
||||
}
|
||||
}catch(e){
|
||||
core.setFailed(e);
|
||||
}
|
||||
|
||||
const latest = semver.valid(semver.coerce('${{ env.LATEST_VERSION }}'));
|
||||
const current = semver.valid(semver.coerce(repository.dot.semver.version));
|
||||
const tag = semver.valid(semver.coerce('${{ env.LATEST_TAG }}'));
|
||||
|
||||
if(latest && latest !== current){
|
||||
core.info(`new ${semver.diff(current, latest)} release found (${latest})!`)
|
||||
repository.dot.semver.version = latest;
|
||||
if(tag){
|
||||
core.exportVariable('WORKFLOW_NEW_TAG', semver.inc(tag, semver.diff(current, latest)));
|
||||
}
|
||||
|
||||
if(repository.dot.semver?.latest){
|
||||
repository.dot.semver.latest = repository.dot.semver.version;
|
||||
}
|
||||
|
||||
if(repository.dot?.readme?.comparison?.image){
|
||||
repository.dot.readme.comparison.image = repository.dot.readme.comparison.image.replace(current, repository.dot.semver.version);
|
||||
}
|
||||
|
||||
try{
|
||||
writeFileSync(resolve('.json'), JSON.stringify(repository.dot, null, 2));
|
||||
core.exportVariable('WORKFLOW_AUTO_UPDATE', true);
|
||||
}catch(e){
|
||||
core.setFailed(e);
|
||||
}
|
||||
}else{
|
||||
core.info('no new release found');
|
||||
}
|
||||
|
||||
core.info(inspect(repository.dot, {showHidden:false, depth:null, colors:true}));
|
||||
|
||||
- name: cron-update / checkout
|
||||
id: checkout
|
||||
if: env.WORKFLOW_AUTO_UPDATE == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add .json
|
||||
git commit -m "[upgrade] ${{ env.LATEST_VERSION }}"
|
||||
git push origin HEAD:master
|
||||
|
||||
- name: cron-update / tag
|
||||
if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success'
|
||||
run: |
|
||||
SHA256=$(git rev-list --branches --max-count=1)
|
||||
git tag -a v${{ env.WORKFLOW_NEW_TAG }} -m "v${{ env.WORKFLOW_NEW_TAG }}" ${SHA256}
|
||||
git push --follow-tags
|
||||
|
||||
- name: cron-update / build docker image
|
||||
if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success'
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
workflow: docker.yml
|
||||
wait-for-completion: false
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
inputs: '{ "release":"true", "readme":"true" }'
|
||||
ref: "v${{ env.WORKFLOW_NEW_TAG }}"
|
439
.github/workflows/docker.yml
vendored
Normal file
439
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,439 @@
|
||||
name: docker
|
||||
run-name: ${{ inputs.run-name }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-name:
|
||||
description: 'set run-name for workflow (multiple calls)'
|
||||
type: string
|
||||
required: false
|
||||
default: 'docker'
|
||||
|
||||
runs-on:
|
||||
description: 'set runs-on for workflow (github or selfhosted)'
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-22.04'
|
||||
|
||||
build:
|
||||
description: 'set WORKFLOW_BUILD'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
release:
|
||||
description: 'set WORKFLOW_GITHUB_RELEASE'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
readme:
|
||||
description: 'set WORKFLOW_GITHUB_README'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
etc:
|
||||
description: 'base64 encoded json string'
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 1440
|
||||
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
ports:
|
||||
- 5000:5000
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: init / checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: init / setup environment
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { existsSync, readFileSync } = require('node:fs');
|
||||
const { resolve } = require('node:path');
|
||||
const { inspect } = require('node:util');
|
||||
const { Buffer } = require('node:buffer');
|
||||
const inputs = `${{ toJSON(github.event.inputs) }}`;
|
||||
const opt = {input:{}, dot:{}};
|
||||
|
||||
try{
|
||||
if(inputs.length > 0){
|
||||
opt.input = JSON.parse(inputs);
|
||||
if(opt.input?.etc){
|
||||
opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii'));
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
core.warning('could not parse github.event.inputs');
|
||||
}
|
||||
|
||||
try{
|
||||
const path = resolve('.json');
|
||||
if(existsSync(path)){
|
||||
try{
|
||||
opt.dot = JSON.parse(readFileSync(path).toString());
|
||||
}catch(e){
|
||||
throw new Error('could not parse .json');
|
||||
}
|
||||
}else{
|
||||
throw new Error('.json does not exist');
|
||||
}
|
||||
}catch(e){
|
||||
core.setFailed(e);
|
||||
}
|
||||
|
||||
core.info(inspect(opt, {showHidden:false, depth:null, colors:true}));
|
||||
|
||||
const docker = {
|
||||
image:{
|
||||
name:opt.dot.image,
|
||||
arch:(opt.dot.arch || 'linux/amd64,linux/arm64'),
|
||||
prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''),
|
||||
suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''),
|
||||
description:(opt.dot?.readme?.description || ''),
|
||||
tags:[],
|
||||
},
|
||||
app:{
|
||||
image:opt.dot.image,
|
||||
name:opt.dot.name,
|
||||
version:(opt.input?.etc?.version || opt.dot?.semver?.version),
|
||||
root:opt.dot.root,
|
||||
UID:(opt.input?.etc?.uid || 1000),
|
||||
GID:(opt.input?.etc?.gid || 1000),
|
||||
no_cache:new Date().getTime(),
|
||||
},
|
||||
cache:{
|
||||
registry:'localhost:5000/',
|
||||
},
|
||||
tags:[],
|
||||
};
|
||||
|
||||
docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`;
|
||||
docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`;
|
||||
docker.app.prefix = docker.image.prefix;
|
||||
docker.app.suffix = docker.image.suffix;
|
||||
|
||||
// setup tags
|
||||
if(!opt.dot?.semver?.disable?.rolling){
|
||||
docker.image.tags.push('rolling');
|
||||
}
|
||||
if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){
|
||||
docker.image.tags.push(`${context.sha.substring(0,7)}`);
|
||||
docker.image.tags.push(opt.input.etc.tag);
|
||||
docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`);
|
||||
docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`;
|
||||
}else if(docker.app.version !== 'latest'){
|
||||
const semver = docker.app.version.split('.');
|
||||
docker.image.tags.push(`${context.sha.substring(0,7)}`);
|
||||
if(Array.isArray(semver)){
|
||||
if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`);
|
||||
if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`);
|
||||
if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`);
|
||||
}
|
||||
if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable');
|
||||
if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest');
|
||||
}else{
|
||||
docker.image.tags.push('latest');
|
||||
}
|
||||
|
||||
for(const tag of docker.image.tags){
|
||||
docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
|
||||
docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
|
||||
docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
|
||||
}
|
||||
|
||||
// setup build arguments
|
||||
if(opt.input?.etc?.build?.args){
|
||||
for(const arg in opt.input.etc.build.args){
|
||||
docker.app[arg] = opt.input.etc.build.args[arg];
|
||||
}
|
||||
}
|
||||
if(opt.dot?.build?.args){
|
||||
for(const arg in opt.dot.build.args){
|
||||
docker.app[arg] = opt.dot.build.args[arg];
|
||||
}
|
||||
}
|
||||
const arguments = [];
|
||||
for(const argument in docker.app){
|
||||
arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`);
|
||||
}
|
||||
|
||||
// export to environment
|
||||
core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry);
|
||||
core.exportVariable('DOCKER_CACHE_NAME', docker.cache.name);
|
||||
core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype);
|
||||
|
||||
core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name);
|
||||
core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch);
|
||||
core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(','));
|
||||
core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description);
|
||||
core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n"));
|
||||
core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile');
|
||||
|
||||
core.exportVariable('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build);
|
||||
core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release);
|
||||
core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme);
|
||||
core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail);
|
||||
core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high'));
|
||||
if(opt.dot?.readme?.comparison){
|
||||
core.exportVariable('WORKFLOW_CREATE_COMPARISON', true);
|
||||
core.exportVariable('WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE', opt.dot.readme.comparison.image);
|
||||
core.exportVariable('WORKFLOW_CREATE_COMPARISON_IMAGE', `${docker.image.name}:${docker.app.version}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
# DOCKER
|
||||
- name: docker / login to hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
|
||||
with:
|
||||
username: 11notes
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: github / login to ghcr
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: 11notes
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: quay / login to quay
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
|
||||
with:
|
||||
registry: quay.io
|
||||
username: 11notes+github
|
||||
password: ${{ secrets.QUAY_TOKEN }}
|
||||
|
||||
- name: docker / setup qemu
|
||||
if: env.WORKFLOW_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a
|
||||
|
||||
- name: docker / setup buildx
|
||||
if: env.WORKFLOW_BUILD == 'true'
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: docker / build & push & tag grype
|
||||
if: env.WORKFLOW_BUILD == 'true'
|
||||
id: docker-build
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_IMAGE_DOCKERFILE }}
|
||||
push: true
|
||||
platforms: ${{ env.DOCKER_IMAGE_ARCH }}
|
||||
cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }}
|
||||
cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true
|
||||
build-args: |
|
||||
${{ env.DOCKER_IMAGE_ARGUMENTS }}
|
||||
tags: |
|
||||
${{ env.DOCKER_CACHE_GRYPE }}
|
||||
|
||||
- name: grype / scan
|
||||
if: env.WORKFLOW_BUILD == 'true'
|
||||
id: grype
|
||||
uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e
|
||||
with:
|
||||
image: ${{ env.DOCKER_CACHE_GRYPE }}
|
||||
fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }}
|
||||
severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }}
|
||||
output-format: 'sarif'
|
||||
by-cve: true
|
||||
cache-db: true
|
||||
|
||||
- name: grype / fail
|
||||
if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure')
|
||||
uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e
|
||||
with:
|
||||
image: ${{ env.DOCKER_CACHE_GRYPE }}
|
||||
fail-build: false
|
||||
severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }}
|
||||
output-format: 'table'
|
||||
by-cve: true
|
||||
cache-db: true
|
||||
|
||||
- name: docker / build & push
|
||||
if: env.WORKFLOW_BUILD == 'true'
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_IMAGE_DOCKERFILE }}
|
||||
push: true
|
||||
sbom: true
|
||||
provenance: mode=max
|
||||
platforms: ${{ env.DOCKER_IMAGE_ARCH }}
|
||||
cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }}
|
||||
cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true
|
||||
build-args: |
|
||||
${{ env.DOCKER_IMAGE_ARGUMENTS }}
|
||||
tags: |
|
||||
${{ env.DOCKER_IMAGE_TAGS }}
|
||||
|
||||
|
||||
|
||||
# RELEASE
|
||||
- name: github / release / log
|
||||
continue-on-error: true
|
||||
id: git-log
|
||||
run: |
|
||||
LOCAL_LAST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`)
|
||||
echo "using last tag: ${LOCAL_LAST_TAG}"
|
||||
LOCAL_COMMITS=$(git log ${LOCAL_LAST_TAG}..HEAD --oneline)
|
||||
|
||||
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||
echo "commits<<${EOF}" >> ${GITHUB_OUTPUT}
|
||||
echo "${LOCAL_COMMITS}" >> ${GITHUB_OUTPUT}
|
||||
echo "${EOF}" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: github / release / markdown
|
||||
if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-log.outcome == 'success'
|
||||
id: git-release
|
||||
uses: 11notes/action-docker-release@v1
|
||||
# WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?!
|
||||
# ---------------------------------------------------------------------------------
|
||||
# the next step "github / release / create" creates a new release based on the code
|
||||
# in the repo. This code is not modified and can't be modified by this action.
|
||||
# It does create the markdown for the release, which could be abused, but to what
|
||||
# extend? Adding a link to a malicious repo?
|
||||
with:
|
||||
git_log: ${{ steps.git-log.outputs.commits }}
|
||||
|
||||
- name: github / release / create
|
||||
if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success'
|
||||
uses: actions/create-release@4c11c9fe1dcd9636620a16455165783b20fc7ea0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
body: ${{ steps.git-release.outputs.release }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
|
||||
|
||||
|
||||
# LICENSE
|
||||
- name: license / update year
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { existsSync, readFileSync, writeFileSync } = require('node:fs');
|
||||
const { resolve } = require('node:path');
|
||||
const file = 'LICENSE';
|
||||
const year = new Date().getFullYear();
|
||||
try{
|
||||
const path = resolve(file);
|
||||
if(existsSync(path)){
|
||||
let license = readFileSync(file).toString();
|
||||
if(!new RegExp(`Copyright \\(c\\) ${year} 11notes`, 'i').test(license)){
|
||||
license = license.replace(/Copyright \(c\) \d{4} /i, `Copyright (c) ${new Date().getFullYear()} `);
|
||||
writeFileSync(path, license);
|
||||
}
|
||||
}else{
|
||||
throw new Error(`file ${file} does not exist`);
|
||||
}
|
||||
}catch(e){
|
||||
core.setFailed(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# README
|
||||
- name: github / checkout HEAD
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git checkout HEAD
|
||||
|
||||
- name: docker / setup comparison images
|
||||
if: env.WORKFLOW_CREATE_COMPARISON == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }}
|
||||
docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size0.log
|
||||
|
||||
docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }}
|
||||
docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size1.log
|
||||
|
||||
docker run --entrypoint "/bin/sh" --rm ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} -c id &> ./comparison.id.log
|
||||
|
||||
- name: github / create README.md
|
||||
id: github-readme
|
||||
continue-on-error: true
|
||||
if: env.WORKFLOW_CREATE_README == 'true'
|
||||
uses: 11notes/action-docker-readme@v1
|
||||
# WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?!
|
||||
# ---------------------------------------------------------------------------------
|
||||
# the next step "github / commit & push" only adds the README and LICENSE as well as
|
||||
# compose.yaml to the repository. This does not pose a security risk if this action
|
||||
# would be compromised. The code of the app can't be changed by this action. Since
|
||||
# only the files mentioned are commited to the repo. Sure, someone could make a bad
|
||||
# compose.yaml, but since this serves only as an example I see no harm in that.
|
||||
with:
|
||||
sarif_file: ${{ steps.grype.outputs.sarif }}
|
||||
build_output_metadata: ${{ steps.docker-build.outputs.metadata }}
|
||||
|
||||
- name: docker / push README.md to docker hub
|
||||
continue-on-error: true
|
||||
if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != ''
|
||||
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8
|
||||
env:
|
||||
DOCKER_USER: 11notes
|
||||
DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }}
|
||||
with:
|
||||
destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }}
|
||||
provider: dockerhub
|
||||
short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }}
|
||||
readme_file: 'README_NONGITHUB.md'
|
||||
|
||||
- name: github / commit & push
|
||||
continue-on-error: true
|
||||
if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != ''
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add README.md
|
||||
if [ -f compose.yaml ]; then
|
||||
git add compose.yaml
|
||||
fi
|
||||
if [ -f LICENSE ]; then
|
||||
git add LICENSE
|
||||
fi
|
||||
git commit -m "github-actions[bot]: update README.md"
|
||||
git push origin HEAD:master
|
||||
|
||||
|
||||
|
||||
|
||||
# REPOSITORY SETTINGS
|
||||
- name: github / update description and set repo defaults
|
||||
run: |
|
||||
curl --request PATCH \
|
||||
--url https://api.github.com/repos/${{ github.repository }} \
|
||||
--header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{
|
||||
"description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}",
|
||||
"homepage":"",
|
||||
"has_issues":true,
|
||||
"has_discussions":true,
|
||||
"has_projects":false,
|
||||
"has_wiki":false
|
||||
}' \
|
||||
--fail
|
16
.github/workflows/readme.yml
vendored
Normal file
16
.github/workflows/readme.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: readme
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
readme:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: update README.md
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
wait-for-completion: false
|
||||
workflow: docker.yml
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
inputs: '{ "build":"false", "release":"false", "readme":"true" }'
|
102
.github/workflows/tags.yml
vendored
Normal file
102
.github/workflows/tags.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: tags
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: build docker image
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
workflow: docker.yml
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
inputs: '{ "release":"true", "readme":"true" }'
|
||||
|
||||
docker-unraid:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: init / base64 nested json
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { Buffer } = require('node:buffer');
|
||||
const etc = {
|
||||
semversuffix:"unraid",
|
||||
uid:99,
|
||||
gid:100,
|
||||
};
|
||||
core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64'));
|
||||
|
||||
- name: build docker image for unraid community
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
workflow: docker.yml
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
inputs: '{ "release":"false", "readme":"false", "run-name":"unraid", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }'
|
||||
|
||||
kms-gui:
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker
|
||||
steps:
|
||||
- name: init / base64 nested json
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { Buffer } = require('node:buffer');
|
||||
(async()=>{
|
||||
try{
|
||||
const master = await fetch('https://raw.githubusercontent.com/11notes/docker-kms/refs/heads/master/.json');
|
||||
const dot = await master.json();
|
||||
const etc = {
|
||||
version:dot.semver.version,
|
||||
};
|
||||
core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64'));
|
||||
}catch(e){
|
||||
core.setFailed(`workflow failed: ${e}`);
|
||||
}
|
||||
})();
|
||||
|
||||
- name: build downstream kms gui
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
workflow: docker.yml
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
repo: 11notes/docker-kms-gui
|
||||
ref: master
|
||||
inputs: '{ "release":"false", "readme":"true", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }'
|
||||
|
||||
kms-gui-unraid:
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker-unraid
|
||||
steps:
|
||||
- name: init / base64 nested json
|
||||
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
|
||||
with:
|
||||
script: |
|
||||
const { Buffer } = require('node:buffer');
|
||||
(async()=>{
|
||||
try{
|
||||
const master = await fetch('https://raw.githubusercontent.com/11notes/docker-kms/refs/heads/master/.json');
|
||||
const dot = await master.json();
|
||||
const etc = {
|
||||
version:dot.semver.version,
|
||||
semversuffix:"unraid",
|
||||
uid:99,
|
||||
gid:100,
|
||||
};
|
||||
core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64'));
|
||||
}catch(e){
|
||||
core.setFailed(`workflow failed: ${e}`);
|
||||
}
|
||||
})();
|
||||
|
||||
- name: build downstream kms gui for unraid community
|
||||
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
|
||||
with:
|
||||
workflow: docker.yml
|
||||
token: "${{ secrets.REPOSITORY_TOKEN }}"
|
||||
repo: 11notes/docker-kms-gui
|
||||
ref: master
|
||||
inputs: '{ "release":"false", "readme":"false", "run-name":"unraid", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }'
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# default
|
||||
maintain/
|
||||
node_modules/
|
||||
|
||||
# custom
|
||||
.env
|
17
.json
Normal file
17
.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"image":"11notes/netbird",
|
||||
"name":"netbird",
|
||||
"root":"/netbird",
|
||||
"arch":"linux/amd64,linux/arm64,linux/arm/v7",
|
||||
|
||||
"semver":{
|
||||
"version":"1.0.2"
|
||||
},
|
||||
|
||||
"readme":{
|
||||
"description":"Activate any version of Windows and Office, forever",
|
||||
"built":{
|
||||
"netbirdio/netbird":"https://github.com/netbirdio/netbird"
|
||||
}
|
||||
}
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 11notes
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
151
arch.dockerfile
Normal file
151
arch.dockerfile
Normal file
@@ -0,0 +1,151 @@
|
||||
# ╔═════════════════════════════════════════════════════╗
|
||||
# ║ SETUP ║
|
||||
# ╚═════════════════════════════════════════════════════╝
|
||||
# GLOBAL
|
||||
ARG APP_UID=1000 \
|
||||
APP_GID=1000 \
|
||||
BUILD_ROOT="/go/netbird/management /go/netbird/relay /go/netbird/signal"
|
||||
|
||||
# :: FOREIGN IMAGES
|
||||
FROM 11notes/nginx:stable AS distroless-nginx
|
||||
FROM 11notes/distroless:curl AS distroless-curl
|
||||
FROM 11notes/util AS util
|
||||
|
||||
# ╔═════════════════════════════════════════════════════╗
|
||||
# ║ BUILD ║
|
||||
# ╚═════════════════════════════════════════════════════╝
|
||||
# :: netbird
|
||||
FROM golang:1.24-alpine AS build
|
||||
COPY --from=util /usr/local/bin /usr/local/bin
|
||||
ARG APP_VERSION \
|
||||
BUILD_ROOT \
|
||||
BUILD_BIN
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
RUN set -ex; \
|
||||
apk --update --no-cache add \
|
||||
build-base \
|
||||
upx \
|
||||
git;
|
||||
|
||||
RUN set -ex; \
|
||||
git clone https://github.com/netbirdio/netbird -b v${APP_VERSION};
|
||||
|
||||
RUN set -ex; \
|
||||
mkdir -p /distroless/usr/local/bin; \
|
||||
for BUILD in ${BUILD_ROOT}; do \
|
||||
cd ${BUILD}; \
|
||||
BIN="${BUILD}/$(echo ${BUILD} | awk -F '/' '{print $4}')"; \
|
||||
go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \
|
||||
eleven checkStatic ${BIN}; \
|
||||
eleven strip ${BIN}; \
|
||||
cp ${BIN} /distroless/usr/local/bin; \
|
||||
done; \
|
||||
mv /distroless/usr/local/bin/management /distroless/usr/local/bin/netbird;
|
||||
|
||||
# :: management
|
||||
FROM golang:1.24-alpine AS management
|
||||
COPY --from=util /usr/local/bin /usr/local/bin
|
||||
COPY ./build/go/management /go/management
|
||||
ENV CGO_ENABLED=0
|
||||
ARG BIN=/go/management/management
|
||||
|
||||
RUN set -ex; \
|
||||
apk --update --no-cache add \
|
||||
build-base \
|
||||
upx;
|
||||
|
||||
RUN set -ex; \
|
||||
cd /go/management; \
|
||||
go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \
|
||||
mkdir -p /distroless/usr/local/bin; \
|
||||
eleven checkStatic ${BIN}; \
|
||||
eleven strip ${BIN}; \
|
||||
cp ${BIN} /distroless/usr/local/bin;
|
||||
|
||||
# :: dashboard
|
||||
FROM golang:1.24-alpine AS dashboard
|
||||
COPY --from=util /usr/local/bin /usr/local/bin
|
||||
COPY ./build/go/dashboard /go/dashboard
|
||||
ENV CGO_ENABLED=0
|
||||
ARG BIN=/go/dashboard/dashboard
|
||||
|
||||
RUN set -ex; \
|
||||
apk --update --no-cache add \
|
||||
build-base \
|
||||
upx \
|
||||
git \
|
||||
nodejs \
|
||||
npm;
|
||||
|
||||
RUN set -ex; \
|
||||
cd /go/dashboard; \
|
||||
go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \
|
||||
mkdir -p /distroless/usr/local/bin; \
|
||||
eleven checkStatic ${BIN}; \
|
||||
eleven strip ${BIN}; \
|
||||
cp ${BIN} /distroless/usr/local/bin;
|
||||
|
||||
RUN set -ex; \
|
||||
git clone https://github.com/netbirdio/dashboard /dashboard;
|
||||
|
||||
RUN set -ex; \
|
||||
cd /dashboard; \
|
||||
npm i --save; \
|
||||
echo '{}' > .local-config.json; \
|
||||
npm run build; \
|
||||
mkdir -p /distroless/nginx/var; \
|
||||
cp -R ./out/* /distroless/nginx/var;
|
||||
|
||||
# :: file system
|
||||
FROM alpine AS file-system
|
||||
COPY --from=util /usr/local/bin /usr/local/bin
|
||||
ARG APP_ROOT
|
||||
USER root
|
||||
|
||||
RUN set -ex; \
|
||||
eleven mkdir /distroless${APP_ROOT}/{etc,var}; \
|
||||
mkdir -p /distroless/var/lib; \
|
||||
ln -sf ${APP_ROOT}/var /distroless/var/lib/netbird;
|
||||
|
||||
|
||||
# ╔═════════════════════════════════════════════════════╗
|
||||
# ║ IMAGE ║
|
||||
# ╚═════════════════════════════════════════════════════╝
|
||||
# :: HEADER
|
||||
FROM scratch
|
||||
|
||||
# :: default arguments
|
||||
ARG TARGETPLATFORM \
|
||||
TARGETOS \
|
||||
TARGETARCH \
|
||||
TARGETVARIANT \
|
||||
APP_IMAGE \
|
||||
APP_NAME \
|
||||
APP_VERSION \
|
||||
APP_ROOT \
|
||||
APP_UID \
|
||||
APP_GID \
|
||||
APP_NO_CACHE
|
||||
|
||||
# :: default environment
|
||||
ENV APP_IMAGE=${APP_IMAGE} \
|
||||
APP_NAME=${APP_NAME} \
|
||||
APP_VERSION=${APP_VERSION} \
|
||||
APP_ROOT=${APP_ROOT}
|
||||
|
||||
# :: multi-stage
|
||||
COPY --from=build /distroless/ /
|
||||
COPY --from=dashboard --chown=${APP_UID}:${APP_GID} /distroless/ /
|
||||
COPY --from=management --chown=${APP_UID}:${APP_GID} /distroless/ /
|
||||
COPY --from=distroless-nginx --chown=${APP_UID}:${APP_GID} / /
|
||||
COPY --from=file-system --chown=${APP_UID}:${APP_GID} /distroless/ /
|
||||
COPY --from=distroless-curl /usr/local/bin /usr/local/bin
|
||||
COPY --chown=${APP_UID}:${APP_GID} ./rootfs/ /
|
||||
|
||||
# :: PERSISTENT DATA
|
||||
VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"]
|
||||
|
||||
# :: EXECUTE
|
||||
USER ${APP_UID}:${APP_GID}
|
2
build/go/dashboard/go.mod
Normal file
2
build/go/dashboard/go.mod
Normal file
@@ -0,0 +1,2 @@
|
||||
module github.com/11notes/docker-netbird
|
||||
go 1.24
|
0
build/go/dashboard/go.sum
Normal file
0
build/go/dashboard/go.sum
Normal file
91
build/go/dashboard/main.go
Normal file
91
build/go/dashboard/main.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const TrustedDomainsTemplate = "/nginx/var/OidcTrustedDomains.js.tmpl"
|
||||
const TrustedDomains = "/nginx/var/OidcTrustedDomains.js"
|
||||
|
||||
func logInfo(s string){
|
||||
log(os.Stdout, fmt.Sprintf("INFO %s", s))
|
||||
}
|
||||
|
||||
func logError(s string){
|
||||
log(os.Stderr, fmt.Sprintf("ERROR %s", s))
|
||||
}
|
||||
|
||||
func log(r io.Writer, s string) {
|
||||
fmt.Fprintf(r, "%s %s\n", time.Now().Format(time.RFC3339), s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// find all the files that contain AUTH_SUPPORTED_SCOPES
|
||||
err := filepath.Walk("/nginx/var",
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text, _ := ioutil.ReadFile(path)
|
||||
if strings.Contains(string(text), "AUTH_SUPPORTED_SCOPES") {
|
||||
replaceEnv(path, string(text))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logError(fmt.Sprintf("filepath.Walk(\"/nginx/var\"): %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// replace custom files
|
||||
custom := []string{TrustedDomainsTemplate}
|
||||
for _, path := range custom {
|
||||
text, _ := ioutil.ReadFile(path)
|
||||
replaceEnv(path, string(text))
|
||||
}
|
||||
|
||||
// rename tmpl
|
||||
os.Rename(TrustedDomainsTemplate, TrustedDomains)
|
||||
|
||||
logInfo("starting dashboard")
|
||||
if err = syscall.Exec("/usr/local/bin/nginx", []string{}, os.Environ()); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEnv(path string, text string){
|
||||
nextjs := []string{"USE_AUTH0", "AUTH_AUDIENCE", "AUTH_AUTHORITY", "AUTH_CLIENT_ID", "AUTH_CLIENT_SECRET", "AUTH_SUPPORTED_SCOPES", "NETBIRD_MGMT_API_ENDPOINT", "NETBIRD_MGMT_GRPC_API_ENDPOINT", "NETBIRD_HOTJAR_TRACK_ID", "NETBIRD_GOOGLE_ANALYTICS_ID", "NETBIRD_GOOGLE_TAG_MANAGER_ID", "AUTH_REDIRECT_URI", "AUTH_SILENT_REDIRECT_URI", "NETBIRD_TOKEN_SOURCE", "NETBIRD_DRAG_QUERY_PARAMS"}
|
||||
|
||||
// replace all environment variables in file
|
||||
for _, e := range os.Environ() {
|
||||
key := strings.Split(e, "=")[0]
|
||||
value := os.Getenv(key)
|
||||
text = string(regexp.MustCompile(fmt.Sprintf(`\$%s`, key)).ReplaceAllString(text, value))
|
||||
}
|
||||
|
||||
// replace all not set environment variables in file
|
||||
for _, e := range nextjs {
|
||||
text = string(regexp.MustCompile(fmt.Sprintf(`\$%s`, e)).ReplaceAllString(text, ""))
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(path, []byte(text), os.ModePerm)
|
||||
if err != nil {
|
||||
logError(fmt.Sprintf("ioutil.WriteFile(%s): %s", path, err))
|
||||
os.Exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
2
build/go/management/go.mod
Normal file
2
build/go/management/go.mod
Normal file
@@ -0,0 +1,2 @@
|
||||
module github.com/11notes/docker-netbird
|
||||
go 1.24
|
0
build/go/management/go.sum
Normal file
0
build/go/management/go.sum
Normal file
49
build/go/management/main.go
Normal file
49
build/go/management/main.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"os"
|
||||
"regexp"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
custom := []string{"/netbird/etc/management.json"}
|
||||
for _, path := range custom {
|
||||
text, _ := ioutil.ReadFile(path)
|
||||
replaceEnv(path, string(text))
|
||||
}
|
||||
if err := syscall.Exec("/usr/local/bin/netbird", []string{"management", "management", "--config", "/netbird/etc/management.json", "--log-file", "console", "--log-level", "info", "--disable-anonymous-metrics", "--disable-geolite-update"}, os.Environ()); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEnv(path string, text string){
|
||||
// replace all environment variables in file
|
||||
for _, e := range os.Environ() {
|
||||
key := strings.Split(e, "=")[0]
|
||||
value := os.Getenv(key)
|
||||
text = string(regexp.MustCompile(fmt.Sprintf(`\${%s}`, key)).ReplaceAllString(text, value))
|
||||
}
|
||||
|
||||
// replace all not set environment variables in file
|
||||
uenv := regexp.MustCompile(`\$\{[A-Z_a-z]+\}`).FindAllString(text, -1)
|
||||
for _, e := range uenv {
|
||||
fmt.Printf("variable %s not set, will be set to empty string!\n", e)
|
||||
text = string(regexp.MustCompile(fmt.Sprintf(`%s`, e)).ReplaceAllString(text, ""))
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(path, []byte(text), os.ModePerm)
|
||||
if err != nil {
|
||||
os.Exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
142
compose.yaml
Normal file
142
compose.yaml
Normal file
@@ -0,0 +1,142 @@
|
||||
name: "netbird"
|
||||
services:
|
||||
db:
|
||||
image: "11notes/postgres:16"
|
||||
read_only: true
|
||||
environment:
|
||||
TZ: "Europe/Zurich"
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- "db.etc:/postgres/etc"
|
||||
- "db.var:/postgres/var"
|
||||
- "db.backup:/postgres/backup"
|
||||
- "db.cmd:/run/cmd"
|
||||
tmpfs:
|
||||
- "/run/postgresql:uid=1000,gid=1000"
|
||||
- "/postgres/log:uid=1000,gid=1000"
|
||||
networks:
|
||||
backend:
|
||||
restart: "always"
|
||||
|
||||
cron:
|
||||
depends_on:
|
||||
db:
|
||||
condition: "service_healthy"
|
||||
restart: true
|
||||
image: "11notes/cron:4.6"
|
||||
environment:
|
||||
TZ: "Europe/Zurich"
|
||||
CRONTAB: |-
|
||||
0 3 * * * cmd-socket '{"bin":"backup"}' > /proc/1/fd/1
|
||||
volumes:
|
||||
- "db.cmd:/run/cmd"
|
||||
restart: "always"
|
||||
|
||||
dashboard:
|
||||
image: "11notes/netbird:0.46.0"
|
||||
read_only: true
|
||||
environment:
|
||||
NETBIRD_MGMT_API_ENDPOINT: "https://${NETBIRD_FQDN}"
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT: "https://${NETBIRD_FQDN}"
|
||||
AUTH_AUDIENCE: "netbird-client"
|
||||
AUTH_CLIENT_ID: "netbird-client"
|
||||
AUTH_CLIENT_SECRET:
|
||||
AUTH_AUTHORITY: "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}"
|
||||
USE_AUTH0: false
|
||||
AUTH_SUPPORTED_SCOPES: "openid"
|
||||
NETBIRD_TOKEN_SOURCE: "accessToken"
|
||||
entrypoint: ["/usr/local/bin/dashboard"]
|
||||
volumes:
|
||||
- "dashboard.var:/nginx/var"
|
||||
tmpfs:
|
||||
- "/nginx/cache:uid=1000,gid=1000"
|
||||
- "/nginx/run:uid=1000,gid=1000"
|
||||
networks:
|
||||
frontend:
|
||||
ports:
|
||||
- "3000:3000/tcp"
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/local/bin/curl", "-kILs", "--fail", "http://localhost:3000/ping"]
|
||||
interval: 5s
|
||||
timeout: 2s
|
||||
start_period: 5s
|
||||
restart: "always"
|
||||
|
||||
management:
|
||||
depends_on:
|
||||
db:
|
||||
condition: "service_healthy"
|
||||
restart: true
|
||||
image: "11notes/netbird:0.46.0"
|
||||
read_only: true
|
||||
env_file: '.env'
|
||||
environment:
|
||||
TZ: "Europe/Zurich"
|
||||
NETBIRD_STORE_ENGINE_POSTGRES_DSN: "host=db user=postgres password=${POSTGRES_PASSWORD} dbname=postgres port=5432"
|
||||
NB_ACTIVITY_EVENT_STORE_ENGINE: "postgres"
|
||||
NB_ACTIVITY_EVENT_POSTGRES_DSN: "host=db user=postgres password=${POSTGRES_PASSWORD} dbname=postgres port=5432"
|
||||
entrypoint: ["/usr/local/bin/management"]
|
||||
volumes:
|
||||
- "management.etc:/netbird/etc"
|
||||
- "management.var:/netbird/var"
|
||||
networks:
|
||||
frontend:
|
||||
backend:
|
||||
ports:
|
||||
- "3080:80/tcp"
|
||||
- "33073:33073/tcp"
|
||||
sysctls:
|
||||
net.ipv4.ip_unprivileged_port_start: 80
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/local/bin/curl", "-kILs", "--fail", "http://localhost:9090/metrics"]
|
||||
interval: 5s
|
||||
timeout: 2s
|
||||
start_period: 5s
|
||||
restart: "always"
|
||||
|
||||
signal:
|
||||
image: "11notes/netbird:0.46.0"
|
||||
environment:
|
||||
TZ: "Europe/Zurich"
|
||||
entrypoint: ["/usr/local/bin/signal"]
|
||||
command: [
|
||||
"run",
|
||||
"--log-file", "console",
|
||||
"--log-level", "info"
|
||||
]
|
||||
volumes:
|
||||
- "signal.var:/netbird/var"
|
||||
networks:
|
||||
frontend:
|
||||
ports:
|
||||
- "10000:10000/tcp"
|
||||
restart: "always"
|
||||
|
||||
relay:
|
||||
image: "11notes/netbird:0.46.0"
|
||||
environment:
|
||||
TZ: "Europe/Zurich"
|
||||
NB_LISTEN_ADDRESS: ":33080"
|
||||
NB_EXPOSED_ADDRESS: "rels://${NETBIRD_FQDN}:443"
|
||||
NB_AUTH_SECRET: ${NETBIRD_RELAY_SECRET}
|
||||
entrypoint: ["/usr/local/bin/relay"]
|
||||
networks:
|
||||
frontend:
|
||||
ports:
|
||||
- "33080:33080/tcp"
|
||||
restart: "always"
|
||||
|
||||
volumes:
|
||||
management.etc:
|
||||
management.var:
|
||||
dashboard.var:
|
||||
signal.var:
|
||||
db.etc:
|
||||
db.var:
|
||||
db.backup:
|
||||
db.cmd:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
backend:
|
||||
internal: true
|
64
project.md
Normal file
64
project.md
Normal file
@@ -0,0 +1,64 @@
|
||||
${{ content_synopsis }} This image will run netbird from a single image (not multiple) rootless and distroless for more security. Due to the nature of a single image and not multiple, you see in the [compose.yaml](https://github.com/11notes/docker-netbird/blob/master/compose.yaml) example that an ```entrypoint:``` has been defined. This image also needs some environment variables present in your **.env** file. This image's defaults (management.json) as well as the example **.env** are to be used with Keycloak as your IdP. You can however provide your own **management.json** file and use any IdP you like.
|
||||
|
||||
${{ github:> [!IMPORTANT] }}
|
||||
${{ github:> }}* This image runs as 1000:1000 by default, most other images run everything as root
|
||||
${{ github:> }}* This image has no shell since it is distroless, most other images run on a distro like Debian or Alpine with full shell access (security)
|
||||
${{ github:> }}* This image does not ship with any critical or high rated CVE and is automatically maintained via CI/CD, most other images mostly have no CVE scanning or code quality tools in place
|
||||
${{ github:> }}* This image is created via a secure, pinned CI/CD process and immune to upstream attacks, most other images have upstream dependencies that can be exploited
|
||||
${{ github:> }}* This image works as read-only, most other images need to write files to the image filesystem
|
||||
${{ github:> }}* This image is a lot smaller than most other images
|
||||
|
||||
If you value security, simplicity and the ability to interact with the maintainer and developer of an image. Using my images is a great start in that direction.
|
||||
|
||||
# COMPARISON 🏁
|
||||
Below you find a comparison between this image and the most used or original one.
|
||||
|
||||
| **image** | 11notes/netbird | netbirdio/* |
|
||||
| ---: | :---: | :---: |
|
||||
| **image size on disk** | 44.6MB | 377.9MB |
|
||||
| **process UID/GID** | 1000/1000 | 0/0 |
|
||||
| **distroless?** | ✅ | ❌ |
|
||||
| **rootless?** | ✅ | ❌ |
|
||||
|
||||
${{ title_volumes }}
|
||||
* **${{ json_root }}/etc** - Directory of your management.json config
|
||||
* **${{ json_root }}/var** - Directory of dynamic data from differnet init systems (relay, signal, management)
|
||||
|
||||
# EXAMPLE ENV FILE 📑
|
||||
```ini
|
||||
# postgres settings
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# netbird settings
|
||||
NETBIRD_RELAY_SECRET=
|
||||
NETBIRD_DATASTORE_ENCRYPTION_KEY=
|
||||
NETBIRD_FQDN=netbird.domain.com
|
||||
|
||||
# Keycloak settings
|
||||
KEYCLOAK_FQDN=keycloak.domain.com
|
||||
KEYCLOAK_REALM=netbird
|
||||
KEYCLOAK_CLIENT_SECRET=
|
||||
|
||||
# STUN/TURN configuration
|
||||
STUN_FQDN_AND_PORT=turn.domain.com:5349
|
||||
TURN_FQDN_AND_PORT=turn.domain.com:5349
|
||||
TURN_SECRET=
|
||||
```
|
||||
|
||||
${{ content_compose }}
|
||||
|
||||
${{ content_defaults }}
|
||||
|
||||
${{ content_environment }}
|
||||
|
||||
${{ content_source }}
|
||||
|
||||
${{ content_parent }}
|
||||
|
||||
${{ content_built }}
|
||||
|
||||
${{ content_tips }}
|
||||
|
||||
${{ title_caution }}
|
||||
${{ github:> [!CAUTION] }}
|
||||
${{ github:> }}* Because this image is distroless, it only works with PostgreSQL, not SQLite. The GeoLocation middleware is also disabled because of this!
|
98
rootfs/netbird/etc/management.json
Normal file
98
rootfs/netbird/etc/management.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"Stuns": [
|
||||
{
|
||||
"Proto": "udp",
|
||||
"URI": "stun:${STUN_FQDN_AND_PORT}",
|
||||
"Secret": "$STUN_SECRET"
|
||||
}
|
||||
],
|
||||
"TURNConfig": {
|
||||
"Turns": [
|
||||
{
|
||||
"Proto": "udp",
|
||||
"URI": "turn:${TURN_FQDN_AND_PORT}"
|
||||
}
|
||||
],
|
||||
"CredentialsTTL": "12h",
|
||||
"Secret": "${TURN_SECRET}",
|
||||
"TimeBasedCredentials": false
|
||||
},
|
||||
"Relay": {
|
||||
"Addresses": ["rels://${NETBIRD_FQDN}:443/relay"],
|
||||
"CredentialsTTL": "24h",
|
||||
"Secret": "${NETBIRD_RELAY_SECRET}"
|
||||
},
|
||||
"Signal": {
|
||||
"Proto": "https",
|
||||
"URI": "${NETBIRD_FQDN}:443",
|
||||
"Username": "",
|
||||
"Password": null
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"TrustedHTTPProxies": [],
|
||||
"TrustedHTTPProxiesCount": 0,
|
||||
"TrustedPeers": [
|
||||
"0.0.0.0/0"
|
||||
]
|
||||
},
|
||||
"Datadir": "/netbird/etc",
|
||||
"DataStoreEncryptionKey": "${NETBIRD_DATASTORE_ENCRYPTION_KEY}",
|
||||
"StoreConfig": {
|
||||
"Engine": "postgres"
|
||||
},
|
||||
"HttpConfig": {
|
||||
"Address": "0.0.0.0:33073",
|
||||
"AuthIssuer": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}",
|
||||
"AuthAudience": "netbird-client",
|
||||
"AuthKeysLocation": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs",
|
||||
"AuthUserIDClaim": "",
|
||||
"IdpSignKeyRefreshEnabled": false,
|
||||
"OIDCConfigEndpoint":"https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/.well-known/openid-configuration"
|
||||
},
|
||||
"IdpManagerConfig": {
|
||||
"ManagerType": "keycloak",
|
||||
"ClientConfig": {
|
||||
"ClientID": "netbird-backend",
|
||||
"ClientSecret": "${KEYCLOAK_CLIENT_SECRET}",
|
||||
"TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token",
|
||||
"GrantType": "client_credentials"
|
||||
},
|
||||
"ExtraConfig": {
|
||||
"AdminEndpoint": "https://${KEYCLOAK_FQDN}/admin/realms/${KEYCLOAK_REALM}",
|
||||
"Auth0ClientCredentials": null,
|
||||
"AzureClientCredentials": null,
|
||||
"KeycloakClientCredentials": null,
|
||||
"ZitadelClientCredentials": null
|
||||
},
|
||||
"DeviceAuthorizationFlow": {
|
||||
"Provider": "hosted",
|
||||
"ProviderConfig": {
|
||||
"Audience": "netbird-client",
|
||||
"AuthorizationEndpoint": "",
|
||||
"Domain": "",
|
||||
"ClientID": "netbird-client",
|
||||
"ClientSecret": "",
|
||||
"TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token",
|
||||
"DeviceAuthEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth/device",
|
||||
"Scope": "openid",
|
||||
"UseIDToken": false,
|
||||
"RedirectURLs": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"PKCEAuthorizationFlow": {
|
||||
"ProviderConfig": {
|
||||
"Audience": "netbird-client",
|
||||
"ClientID": "netbird-client",
|
||||
"ClientSecret": "",
|
||||
"Domain": "",
|
||||
"AuthorizationEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth",
|
||||
"TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token",
|
||||
"Scope": "openid",
|
||||
"RedirectURLs": ["http://localhost:53000"],
|
||||
"UseIDToken": false,
|
||||
"DisablePromptLogin": false,
|
||||
"LoginFlag": 1
|
||||
}
|
||||
}
|
||||
}
|
21
rootfs/nginx/etc/default.conf
Normal file
21
rootfs/nginx/etc/default.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 3000 default_server;
|
||||
root /nginx/var;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
expires off;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
expires off;
|
||||
}
|
||||
|
||||
location /ping {
|
||||
return 200;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user