Mirror Docker images to a custom registry
This guide explains how to prepare container images for WorkAdventure in a Kubernetes cluster without internet access.
Why this matters
This workflow is useful for secured Kubernetes installations where cluster nodes cannot pull images from the internet.
High-level flow:
- Load required images from the internet onto a bridge machine.
- Push those images from the bridge machine to a secured internal registry.
- Configure Kubernetes to pull images from that secured internal registry.
The approach is:
- Render a Helm chart from its remote
.tgzURL with your real values. - Extract all container images from the rendered manifests.
- Optionally mirror those images to your private registry.
- Override image repositories in your values files so Kubernetes only pulls from your private registry.
Run the script once for the admin chart and once for the synapse chart.
Prerequisites
You need:
helmrg(ripgrep)docker(orskopeo) only if you run in mirror mode
You also need:
- Source registry credentials (including WorkAdventure private registry credentials for admin images) if you run in mirror mode
- Credentials to push to your private registry if you run in mirror mode
1. Prepare values files and chart URLs
Create and maintain values files with your final production configuration:
values-admin.yamlvalues-synapse.yaml
For exhaustive image discovery (all optional components enabled), you can use the following minimal values files:
# values-admin.yaml
domainName: "example.com"
backup:
enabled: true
phpmyadmin:
enabled: true
workadventure:
enabled: true
livekit:
enabled: true
credentials:
apiKey: "livekit_api_key"
apiSecret: "livekit_api_secret"
egress:
enabled: true
livekit-egress:
egress:
api_key: "livekit_api_key"
api_secret: "livekit_api_secret"
rustfs:
enabled: true
# values-synapse.yaml
synapse:
serverName: "matrix.example.com"
publicServerName: "matrix.example.com"
backup:
enable: true
Use the chart package URLs corresponding to the version you install.
Examples:
- Admin:
https://charts.workadventu.re/charts/workadventure-admin-v1.30.44.tgz - Synapse:
https://charts.workadventu.re/charts/workadventure-synapse-v1.30.44.tgz
Replace v1.30.44 with the chart versions you actually deploy.
You can look up available versions in https://charts.workadventu.re/index.yaml.
2. List / mirror script
Create a script named scripts/mirror-images.sh:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 && $# -ne 3 ]]; then
echo "Usage (list only): $0 <chart-url.tgz> <values.yaml>"
echo "Usage (mirror): $0 <chart-url.tgz> <values.yaml> <local-registry-host:port>"
exit 1
fi
LOCAL_REGISTRY=""
if [[ $# -eq 2 ]]; then
CHART_URL="$1"
VALUES_FILE="$2"
else
CHART_URL="$1"
VALUES_FILE="$2"
LOCAL_REGISTRY="$3"
fi
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
normalize_image_ref() {
# Output is always explicit and fully qualified, with tag/digest.
# Examples:
# - busybox -> docker.io/library/busybox:latest
# - bitnamilegacy/redis:7 -> docker.io/bitnamilegacy/redis:7
# - ghcr.io/element-hq/synapse:v1 -> ghcr.io/element-hq/synapse:v1
local image="$1"
local name ref first
if [[ "$image" == *@* ]]; then
name="${image%@*}"
ref="@${image#*@}"
elif [[ "${image##*/}" == *:* ]]; then
name="${image%:*}"
ref=":${image##*:}"
else
name="$image"
ref=":latest"
fi
first="${name%%/*}"
if [[ "$first" == *.* || "$first" == *:* || "$first" == "localhost" ]]; then
# Already contains a registry host.
:
else
# Docker Hub shorthand.
if [[ "$name" == */* ]]; then
name="docker.io/${name}"
else
name="docker.io/library/${name}"
fi
fi
echo "${name}${ref}"
}
to_local_ref() {
# Keep source registry host in the path to avoid collisions across registries.
# The host is sanitized (':' -> '-') because ':' is not allowed in repository paths.
# Example:
# ghcr.io/element-hq/synapse:v1 -> <local>/ghcr.io/element-hq/synapse:v1
# git.thecodingmachine.com:444/repo/img:tag -> <local>/git.thecodingmachine.com-444/repo/img:tag
local normalized="$1"
local name ref host repo host_path
if [[ "$normalized" == *@* ]]; then
name="${normalized%@*}"
ref="@${normalized#*@}"
else
name="${normalized%:*}"
ref=":${normalized##*:}"
fi
host="${name%%/*}"
repo="${name#*/}"
host_path="${host//:/-}"
echo "${LOCAL_REGISTRY}/${host_path}/${repo}${ref}"
}
CHART_FILE="$(basename "${CHART_URL%%\?*}")"
CHART_ID="${CHART_FILE%.tgz}"
CHART_ID="$(echo "$CHART_ID" | sed -E 's/[^a-zA-Z0-9._-]+/_/g')"
RELEASE_NAME="$(echo "$CHART_ID" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
if [[ -z "$RELEASE_NAME" ]]; then
RELEASE_NAME="chart"
fi
RENDERED="$WORKDIR/${CHART_ID}.yaml"
ALL_IMAGES_RAW="$WORKDIR/images-raw.txt"
ALL_IMAGES_NORM="$WORKDIR/images-normalized.txt"
OUTPUT_IMAGES_FILE="images-${CHART_ID}.txt"
helm template "$RELEASE_NAME" "$CHART_URL" -f "$VALUES_FILE" --include-crds --skip-tests > "$RENDERED"
cat "$RENDERED" \
| rg '^[[:space:]]*image:' \
| sed -E 's/^[[:space:]]*image:[[:space:]]*//; s/^"//; s/"$//' \
| sort -u > "$ALL_IMAGES_RAW"
while read -r img; do
[[ -z "$img" ]] && continue
normalize_image_ref "$img"
done < "$ALL_IMAGES_RAW" | sort -u > "$ALL_IMAGES_NORM"
cp "$ALL_IMAGES_NORM" "$OUTPUT_IMAGES_FILE"
if [[ -z "$LOCAL_REGISTRY" ]]; then
echo "Done (list only)."
echo "- Image list written to: $OUTPUT_IMAGES_FILE"
echo
cat "$OUTPUT_IMAGES_FILE"
exit 0
fi
OUTPUT_MAP_FILE="image-map-${CHART_ID}.txt"
: > "$OUTPUT_MAP_FILE"
while read -r src; do
[[ -z "$src" ]] && continue
dst="$(to_local_ref "$src")"
echo "Mirroring: $src -> $dst"
if command -v skopeo >/dev/null 2>&1; then
skopeo copy --all "docker://$src" "docker://$dst"
else
docker pull "$src"
docker tag "$src" "$dst"
docker push "$dst"
fi
echo "$src=$dst" >> "$OUTPUT_MAP_FILE"
done < "$ALL_IMAGES_NORM"
echo
echo "Done (mirror mode)."
echo "- Image mapping written to: $OUTPUT_MAP_FILE"
echo "- Image list written to: $OUTPUT_IMAGES_FILE"
echo "- Source image list written to: $ALL_IMAGES_RAW"
echo "- Normalized image list written to: $ALL_IMAGES_NORM"
Make it executable:
chmod +x scripts/mirror-images.sh
Using the script
The script has 2 modes:
- List-only mode:
./mirror-images.sh <chart-url> <values.yaml>it generates a list of all images used by the chart with your values. This is useful for auditing and for manual mirroring. - Mirror mode:
./mirror-images.sh <chart-url> <values.yaml> <registry-url>it mirrors all images to your own registry and generates a mapping file from source to destination references. This is useful for automation.
Run in list-only mode (example for admin chart):
scripts/mirror-images.sh \
https://charts.workadventu.re/charts/workadventure-admin-v1.30.44.tgz \
values-admin.yaml
Run in list-only mode for synapse:
scripts/mirror-images.sh \
`https://charts.workadventu.re/charts/workadventure-synapse-v1.30.44.tgz` \
values-synapse.yaml
This creates one image list per chart in the current directory and prints it:
images-workadventure-admin-v1.30.44.txtimages-workadventure-synapse-v1.30.44.txt
Run in mirror mode:
# Authenticate to source registries and destination registry first
# docker login git.thecodingmachine.com:444
# docker login <local-registry-host:port>
scripts/mirror-images.sh \
https://charts.workadventu.re/charts/workadventure-admin-v1.30.44.tgz \
values-admin.yaml \
registry.internal.example.com:5000
scripts/mirror-images.sh \
https://charts.workadventu.re/charts/workadventure-synapse-v1.30.44.tgz \
values-synapse.yaml \
registry.internal.example.com:5000
This creates one image map per chart in the current directory:
image-map-workadventure-admin-v1.30.44.txtimage-map-workadventure-synapse-v1.30.44.txt
3. Override images in values files
In mirror mode, use the generated image-map-*.txt files to update image repository keys to your local registry.
Admin chart keys to override
In your admin values file, review and override at least:
admin.image.repositoryheadlessBrowser.image.repositorybackup.image.repositoryrustfs.image.repositoryrustfs.volumePermissions.image.repositoryrustfs.initJob.image.repositoryworkadventure.play.image.repositoryworkadventure.back.image.repositoryworkadventure.uploader.image.repositoryworkadventure.maps.image.repository(if enabled)workadventure.icon.image.repositoryworkadventure.mapstorage.image.repositoryworkadventure.redis.image.repositoryworkadventure.redis.backup.image.repository
For Bitnami dependencies in admin chart, if you mirrored while preserving repository names, you can use:
redis.global.imageRegistrymysql.global.imageRegistryphpmyadmin.global.imageRegistry
Example:
imagePullSecrets:
- name: local-registry-pull
admin:
image:
repository: registry.internal.example.com:5000/git.thecodingmachine.com-444/tcm-projects/workadventure-saas/admin
headlessBrowser:
image:
repository: registry.internal.example.com:5000/git.thecodingmachine.com-444/tcm-projects/workadventure-saas/headless-browser
redis:
global:
imageRegistry: registry.internal.example.com:5000/docker.io
mysql:
global:
imageRegistry: registry.internal.example.com:5000/docker.io
phpmyadmin:
global:
imageRegistry: registry.internal.example.com:5000/docker.io
Synapse chart keys to override
In your synapse values file, override:
backup.image.repositorycreateMatrixUserJob.image.repositorysynapse.image.repositorysynapse.signingkey.job.generateImage.repositorysynapse.signingkey.job.publishImage.repositorysynapse.wellknown.image.repositorysynapse.postgresql.image.repositorysynapse.redis.image.repositorysynapse.volumePermissions.image.repository(if enabled)
Example:
imagePullSecrets:
- name: local-registry-pull
backup:
image:
repository: registry.internal.example.com:5000/docker.io/eeshugerman/postgres-backup-s3
createMatrixUserJob:
image:
repository: registry.internal.example.com:5000/docker.io/bitnamilegacy/kubectl
synapse:
serverName: "matrix.example.com"
publicServerName: "matrix.example.com"
image:
repository: registry.internal.example.com:5000/ghcr.io/element-hq/synapse
signingkey:
job:
generateImage:
repository: registry.internal.example.com:5000/docker.io/matrixdotorg/synapse
publishImage:
repository: registry.internal.example.com:5000/docker.io/bitnami/kubectl
wellknown:
image:
repository: registry.internal.example.com:5000/ghcr.io/rtsp/docker-lighttpd
postgresql:
image:
repository: registry.internal.example.com:5000/docker.io/bitnamilegacy/postgresql
redis:
image:
repository: registry.internal.example.com:5000/docker.io/bitnamilegacy/redis
4. Validate before installation
Ensure no external image remains:
helm template admin https://charts.workadventu.re/charts/workadventure-admin-v1.30.44.tgz -f values-admin.yaml --skip-tests \
| rg '^[[:space:]]*image:' \
| rg -v 'registry.internal.example.com:5000'
helm template synapse https://charts.workadventu.re/charts/workadventure-synapse-v1.30.44.tgz -f values-synapse.yaml --skip-tests \
| rg '^[[:space:]]*image:' \
| rg -v 'registry.internal.example.com:5000'
Both commands should return no output.
5. Install
With your new values.yaml files, install should be done as described in the "admin self-hosting guide"
Notes
- If your values enable or disable components, rerun the mirroring script after each change.
- If chart versions change, rerun the script because image lists may change.