A complete Jenkins build system setup using Incus containers, designed for seamless
cross-architecture CI pipelines across two hosts: amd64.us (x86_64) and arm64.us (aarch64).
Supports Docker, Podman, cross-compilation, multi-registry Docker pushes, and
dynamic configuration driven from Git metadata.
| Component | Host | Architecture | Purpose |
|---|---|---|---|
| Jenkins Main | casjay.cc | x86_64 | Central Jenkins Controller |
| Build Node | amd64.us | amd64 / x86_64 | Docker/Podman agents, cross-agent |
| Build Node | arm64.us | arm64 / aarch64 | Docker/Podman agents, cross-agent |
Ensure required cgroup settings for nested containers:
if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then
sed -i 's/^GRUB_CMDLINE_LINUX="*/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1 /' /etc/default/grub
else
echo 'GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1"' >> /etc/default/grub
figrub2-mkconfig -o /boot/grub2/grub.cfgfor grub_efi in $(ls -A /boot/efi/EFI/*/grub.cfg 2>/dev/null); do
if [ -n "$grub_efi" ] && [ -f "$grub_efi" ]; then
grub2-mkconfig -o "$grub_efi"
fi
donerebootstat -fc %T /sys/fs/cgroup 2>/dev/null| grep -q "cgroup2fs" && echo "cgroup2fs is enabled" || echo "cgroup2fs is disabled" dnf install -y java-17-openjdk git jenkinssystemctl enable --now jenkinssudo -u jenkins ssh-keygen -t ed25519 -f /var/lib/jenkins/.ssh/id_ed25519 -N ""Configure under Manage Jenkins → Configure System → Global properties → Environment variables: http://casjay.cc/configure
| Variable | Example Value | Description |
|---|---|---|
REGISTRIES |
docker.io ghcr.io dockersrc.us |
List of Docker registries |
REGISTERY_ORG_USER |
casjaysdev |
Default organization/user prefix |
ORG_PREFIX_DOCKER_IO |
casjaysdev |
Override for docker.io |
ORG_PREFIX_GHCR_IO |
casjaysdev |
Override for ghcr.io |
ORG_PREFIX_DOCKERSRC_US |
casjaysdev |
Override for dockersrc.us |
TAG_LATEST |
latest |
“latest” tag |
IMAGE_AUTHORS |
CasjaysDev CI/CD <ci@casjay.cc> |
OCI image authors metadata |
IMAGE_LICENSES |
WTFPL |
OCI image license metadata |
- Navigate to Manage Jenkins > Credentials > Global: https://casjay.cc/credentials/store/system/domain/_/
- Add credential:
- Kind: SSH Username with Private Key
- Username:
jenkins - Private Key: Paste content of
/var/lib/jenkins/.ssh/id_ed25519 - ID:
jenkins-ssh
https://casjay.cc/user/administrator/security/
https://casjay.cc/manage/pluginManager/available
docker-plugin
docker-workflow
docker-commons
Ensure required cgroup settings for nested containers:
if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then
sed -i 's/^GRUB_CMDLINE_LINUX="*/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1 /' /etc/default/grub
else
echo 'GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1"' >> /etc/default/grub
figrub2-mkconfig -o /boot/grub2/grub.cfgfor grub_efi in $(ls -A /boot/efi/EFI/*/grub.cfg 2>/dev/null); do
if [ -n "$grub_efi" ] && [ -f "$grub_efi" ]; then
grub2-mkconfig -o "$grub_efi"
fi
donerebootstat -fc %T /sys/fs/cgroup 2>/dev/null| grep -q "cgroup2fs" && echo "cgroup2fs is enabled" || echo "cgroup2fs is disabled" dnf install -y java-17-openjdk git openssh-serversystemctl enable --now sshduseradd --system --create-home --shell /bin/bash jenkinsmkdir -p /home/jenkins/.sshchmod 700 /home/jenkins/.sshchown -Rf jenkins:jenkins /home/jenkinspasswd jenkinspasswd -u jenkinsssh -l root casjay.cc 'ssh-copy-id -i /var/lib/jenkins/.ssh/id_ed25519.pub jenkins@amd64.us'usermod -aG docker jenkinsALPINE_VERSION="${ALPINE_VERSION:-3.20}"DEBIAN_VERSION="${DEBIAN_VERSION:-bookworm}"Docker Agent (native):
incus launch images:alpine/${ALPINE_VERSION} docker-agentincus config set docker-agent security.nesting trueincus config set docker-agent security.privileged trueincus exec docker-agent -- apk add --no-cache docker openrcincus exec docker-agent -- rc-update add docker defaultincus exec docker-agent -- service docker startPodman Agent:
incus launch images:alpine/${ALPINE_VERSION} podman-agentincus config set podman-agent security.nesting trueincus exec podman-agent -- apk add --no-cache podman fuse-overlayfs shadowincus exec podman-agent -- rc-service cgroups startCross Agent:
incus launch images:debian/${DEBIAN_VERSION} cross-agentincus exec cross-agent -- bash -c "apt-get update && apt-get update -yy && apt-get dist-upgrade -yy"incus exec cross-agent -- bash -c "apt-get install -y build-essential clang cmake ninja-build gcc-mingw-w64 g++-mingw-w64 gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binfmt-support qemu-user-static rustc cargo golang-go python3 python3-pip ruby ruby-dev openjdk-17-jdk maven gradle mono-complete php-cli composer"Ensure required cgroup settings for nested containers:
if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then
sed -i 's/^GRUB_CMDLINE_LINUX="*/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1 /' /etc/default/grub
else
echo 'GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 cgroup_enable=cpuset cgroup_enable=memory swapaccount=1"' >> /etc/default/grub
figrub2-mkconfig -o /boot/grub2/grub.cfgfor grub_efi in $(ls -A /boot/efi/EFI/*/grub.cfg 2>/dev/null); do
if [ -n "$grub_efi" ] && [ -f "$grub_efi" ]; then
grub2-mkconfig -o "$grub_efi"
fi
donerebootstat -fc %T /sys/fs/cgroup 2>/dev/null| grep -q "cgroup2fs" && echo "cgroup2fs is enabled" || echo "cgroup2fs is disabled" dnf install -y java-17-openjdk git openssh-serversystemctl enable --now sshduseradd --system --create-home --shell /bin/bash jenkinsmkdir -p /home/jenkins/.sshchmod 700 /home/jenkins/.sshchown -Rf jenkins:jenkins /home/jenkinspasswd jenkinspasswd -u jenkinsusermod -aG docker jenkinscurl -L -o install_plugins.sh https://raw.githubusercontent.com/jenkinsci/plugin-installation-manager-tool/master/install-plugins.sh -O /usr/local/jenkins_plugins && \
chmod +x /usr/local/jenkins_pluginsssh -l root casjay.cc 'ssh-copy-id -i /var/lib/jenkins/.ssh/id_ed25519.pub jenkins@arm64.us'ALPINE_VERSION="${ALPINE_VERSION:-3.20}"DEBIAN_VERSION="${DEBIAN_VERSION:-bookworm}"Docker Agent (native):
incus launch images:alpine/${ALPINE_VERSION} docker-agentincus config set docker-agent security.nesting trueincus config set docker-agent security.privileged trueincus exec docker-agent -- apk add --no-cache docker openrcincus exec docker-agent -- rc-update add docker defaultincus exec docker-agent -- service docker startPodman Agent:
incus launch images:alpine/${ALPINE_VERSION} podman-agentincus config set podman-agent security.nesting trueincus exec podman-agent -- apk add --no-cache podman fuse-overlayfs shadowincus exec podman-agent -- rc-service cgroups startCross Agent:
incus launch images:debian/${DEBIAN_VERSION} cross-agentincus exec cross-agent -- bash -c "apt-get update && apt-get update -yy && apt-get dist-upgrade -yy"incus exec cross-agent -- bash -c "apt-get install -y build-essential clang cmake ninja-build gcc-mingw-w64 g++-mingw-w64 gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binfmt-support qemu-user-static rustc cargo golang-go python3 python3-pip ruby ruby-dev openjdk-17-jdk maven gradle mono-complete php-cli composer"FROM debian:bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential clang cmake ninja-build \
gcc-mingw-w64 g++-mingw-w64 \
gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
qemu-user-static binfmt-support \
rustc cargo golang-go python3 python3-pip \
ruby ruby-dev openjdk-17-jdk maven \
gradle dotnet-sdk-7.0 mono-complete \
php-cli composer zig \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /build
CMD ["/bin/bash"]pipeline {
agent any
environment {
IMAGE_REPO = ""
TAG_LATEST = "${env.TAG_LATEST ?: 'latest'}"
TAG_DATE = "${env.TAG_DATE ?: new Date().format('yyMM')}"
TAG_COMMIT = "${env.TAG_COMMIT ?: (env.GIT_COMMIT?.substring(0,7) ?: 'dev')}"
IMAGE_LICENSES = "${env.IMAGE_LICENSES ?: 'WTFPL'}"
IMAGE_AUTHORS = "${env.IMAGE_AUTHORS ?: 'Jason Hempstead <dev@casjay.cc>'}"
REGISTRIES = "${env.REGISTRIES ?: 'docker.io ghcr.io dockersrc.us'}"
REGISTERY_ORG_USER = "${env.REGISTERY_ORG_USER ?: 'casjaysdev'}"
ORG_PREFIX_GHCR_IO = "${env.ORG_PREFIX_GHCR_IO ?: 'casjaysdev'}"
ORG_PREFIX_DOCKER_IO = "${env.ORG_PREFIX_DOCKER_IO ?: 'casjaysdev'}"
ORG_PREFIX_DOCKERSRC_US = "${env.ORG_PREFIX_DOCKERSRC_US ?: 'library'}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Set Dynamic Repo and Org/User') {
steps {
script {
def gitUrl = env.GIT_URL ?: sh(script: "git config --get remote.origin.url", returnStdout: true).trim()
def repoName = gitUrl.tokenize('/').last().replace('.git', '')
def orgUser = ''
if (gitUrl.contains('@')) {
orgUser = gitUrl.tokenize(':').last().tokenize('/')[0]
} else if (gitUrl.contains('://')) {
orgUser = gitUrl.tokenize('/')[3]
}
env.IMAGE_REPO = env.IMAGE_REPO ?: repoName
env.DEFAULT_ORG_USER = env.REGISTERY_ORG_USER ?: orgUser
echo "Determined IMAGE_REPO = ${env.IMAGE_REPO}"
echo "Using DEFAULT_ORG_USER = ${env.DEFAULT_ORG_USER}"
}
}
}
stage('Build, Push & Manifest') {
steps {
script {
def registries = env.REGISTRIES.split()
def tags = [env.TAG_LATEST, env.TAG_DATE, env.TAG_COMMIT]
registries.each { registry ->
def envVarName = "ORG_PREFIX_" + registry.replaceAll(/[\.\-]/, "_").toUpperCase()
def prefix = env[envVarName] ?: env.DEFAULT_ORG_USER
if (!prefix) {
error("No org/user prefix found for registry '${registry}' and no REGISTERY_ORG_USER set.")
}
tags.each { tag ->
def fullTagAmd64 = "${registry}/${prefix}/${env.IMAGE_REPO}:${tag}-amd64"
def fullTagArm64 = "${registry}/${prefix}/${env.IMAGE_REPO}:${tag}-aarch64"
echo "Building and pushing ${fullTagAmd64} and ${fullTagArm64}"
sh """
docker build --platform linux/amd64 -t ${fullTagAmd64} .
docker push ${fullTagAmd64}
docker build --platform linux/arm64 -t ${fullTagArm64} .
docker push ${fullTagArm64}
"""
def wikiUrl = ""
if (gitUrl) {
def baseGitUrl = gitUrl.replaceFirst(/\.git$/, '')
wikiUrl = baseGitUrl + "/wiki"
}
sh """
docker manifest create ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} \
${fullTagAmd64} \
${fullTagArm64}
"""
sh """
docker manifest annotate ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} ${fullTagAmd64} \
--os linux \
--arch amd64 \
--annotation org.opencontainers.image.title="${env.IMAGE_REPO}" \
--annotation org.opencontainers.image.version="${tag}" \
--annotation org.opencontainers.image.revision="${env.TAG_COMMIT}" \
--annotation org.opencontainers.image.created="${new Date().format('yyyy-MM-dd\\'T\\'HH:mm:ssXXX')}" \
--annotation org.opencontainers.image.authors="${env.IMAGE_AUTHORS}" \
--annotation org.opencontainers.image.documentation="${wikiUrl}" \
--annotation org.opencontainers.image.url="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.source="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.licenses="${env.IMAGE_LICENSES}"
"""
sh """
docker manifest annotate ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} ${fullTagArm64} \
--os linux \
--arch arm64 \
--annotation org.opencontainers.image.title="${env.IMAGE_REPO}" \
--annotation org.opencontainers.image.version="${tag}" \
--annotation org.opencontainers.image.revision="${env.TAG_COMMIT}" \
--annotation org.opencontainers.image.created="${new Date().format('yyyy-MM-dd\\'T\\'HH:mm:ssXXX')}" \
--annotation org.opencontainers.image.authors="${env.IMAGE_AUTHORS}" \
--annotation org.opencontainers.image.documentation="${wikiUrl}" \
--annotation org.opencontainers.image.url="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.source="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.licenses="${env.IMAGE_LICENSES}"
"""
sh "docker manifest push ${registry}/${prefix}/${env.IMAGE_REPO}:${tag}"
}
}
}
}
}
}
}pipeline {
agent any
environment {
IMAGE_REPO = ""
TAG_LATEST = "${env.TAG_LATEST ?: 'latest'}"
TAG_DATE = "${env.TAG_DATE ?: new Date().format('yyMM')}"
TAG_COMMIT = "${env.TAG_COMMIT ?: (env.GIT_COMMIT?.substring(0,7) ?: 'dev')}"
IMAGE_LICENSES = "${env.IMAGE_LICENSES ?: 'WTFPL'}"
IMAGE_AUTHORS = "${env.IMAGE_AUTHORS ?: 'Jason Hempstead <dev@casjay.cc>'}"
REGISTRIES = "${env.REGISTRIES ?: 'docker.io ghcr.io dockersrc.us'}"
REGISTERY_ORG_USER = "${env.REGISTERY_ORG_USER ?: 'casjaysdevdocker'}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Set Dynamic Repo and Org/User') {
steps {
script {
def gitUrl = env.GIT_URL ?: sh(script: "git config --get remote.origin.url", returnStdout: true).trim()
def repoName = gitUrl.tokenize('/').last().replace('.git', '')
def orgUser = ''
if (gitUrl.contains('@')) {
orgUser = gitUrl.tokenize(':').last().tokenize('/')[0]
} else if (gitUrl.contains('://')) {
orgUser = gitUrl.tokenize('/')[3]
}
env.IMAGE_REPO = env.IMAGE_REPO ?: repoName
env.DEFAULT_ORG_USER = env.REGISTERY_ORG_USER ?: orgUser
echo "Determined IMAGE_REPO = ${env.IMAGE_REPO}"
echo "Using DEFAULT_ORG_USER = ${env.DEFAULT_ORG_USER}"
}
}
}
stage('Build, Push & Manifest') {
steps {
script {
def registries = env.REGISTRIES.split()
def tags = [env.TAG_LATEST, env.TAG_DATE, env.TAG_COMMIT]
registries.each { registry ->
def envVarName = "ORG_PREFIX_" + registry.replaceAll(/[\.\-]/, "_").toUpperCase()
def prefix = env[envVarName] ?: env.DEFAULT_ORG_USER
if (!prefix) {
error("No org/user prefix found for registry '${registry}' and no REGISTERY_ORG_USER set.")
}
tags.each { tag ->
def fullTagAmd64 = "${registry}/${prefix}/${env.IMAGE_REPO}:${tag}-amd64"
def fullTagArm64 = "${registry}/${prefix}/${env.IMAGE_REPO}:${tag}-aarch64"
echo "Building and pushing ${fullTagAmd64} and ${fullTagArm64}"
sh """
docker build --platform linux/amd64 -t ${fullTagAmd64} .
docker push ${fullTagAmd64}
docker build --platform linux/arm64 -t ${fullTagArm64} .
docker push ${fullTagArm64}
"""
def wikiUrl = ""
if (gitUrl) {
def baseGitUrl = gitUrl.replaceFirst(/\.git$/, '')
wikiUrl = baseGitUrl + "/wiki"
}
sh """
docker manifest create ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} \
${fullTagAmd64} \
${fullTagArm64}
"""
sh """
docker manifest annotate ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} ${fullTagAmd64} \
--os linux \
--arch amd64 \
--annotation org.opencontainers.image.title="${env.IMAGE_REPO}" \
--annotation org.opencontainers.image.version="${tag}" \
--annotation org.opencontainers.image.revision="${env.TAG_COMMIT}" \
--annotation org.opencontainers.image.created="${new Date().format('yyyy-MM-dd\\'T\\'HH:mm:ssXXX')}" \
--annotation org.opencontainers.image.authors="${env.IMAGE_AUTHORS}" \
--annotation org.opencontainers.image.documentation="${wikiUrl}" \
--annotation org.opencontainers.image.url="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.source="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.licenses="${env.IMAGE_LICENSES}"
"""
sh """
docker manifest annotate ${registry}/${prefix}/${env.IMAGE_REPO}:${tag} ${fullTagArm64} \
--os linux \
--arch arm64 \
--annotation org.opencontainers.image.title="${env.IMAGE_REPO}" \
--annotation org.opencontainers.image.version="${tag}" \
--annotation org.opencontainers.image.revision="${env.TAG_COMMIT}" \
--annotation org.opencontainers.image.created="${new Date().format('yyyy-MM-dd\\'T\\'HH:mm:ssXXX')}" \
--annotation org.opencontainers.image.authors="${env.IMAGE_AUTHORS}" \
--annotation org.opencontainers.image.documentation="${wikiUrl}" \
--annotation org.opencontainers.image.url="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.source="${gitUrl ?: ''}" \
--annotation org.opencontainers.image.licenses="${env.IMAGE_LICENSES}"
"""
sh "docker manifest push ${registry}/${prefix}/${env.IMAGE_REPO}:${tag}"
}
}
}
}
}
}
}