Add custom Docker registry and hardened image support

All image references in docker-compose.yml and docker-compose.local.yml
are replaced with ${IMAGE_VAR:-default} env var syntax, so compose files
work standalone without .env while deploy.sh writes resolved image paths.

deploy.sh gains two new prompts:
- Custom registry prefix (prepended to all image names)
- Hardened images from dhi.io for Redis/PostgreSQL/Caddy (takes priority
  over custom registry for those three)

Compared to PR #10: interactive prompts instead of hardcoded vars,
no sed-based compose file mutation, all 14 images covered (PR #10 missed
element-admin, element-call, lk-jwt-service, and all 3 bridges), and
standalone compose usage is preserved via :-default fallbacks.

SETUP.md and README.md document the feature including a note on
pull-through cache registries (Harbor/Artifactory/Nexus) that require
the full docker.io/ path prefix in image names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wmair
2026-03-02 09:55:18 +01:00
parent e789b845cb
commit 4e43a6f713
5 changed files with 178 additions and 26 deletions
+6
View File
@@ -109,6 +109,12 @@ TELEGRAM_API_HASH=your_hash
Bridges use double puppet support (messages appear from your actual Matrix user, not a bridge bot) and have encryption disabled for compatibility with MAS. See [BRIDGE_SETUP_GUIDE.md](BRIDGE_SETUP_GUIDE.md) for details.
## Air-gapped / Custom Registry
`deploy.sh` optionally prefixes all image references with a custom registry URL (for internal mirrors or air-gapped environments) and optionally switches Redis, PostgreSQL, and Caddy to hardened variants from [dhi.io](https://dhi.io). Both settings are written to `.env` and picked up automatically by Docker Compose.
See [SETUP.md — Custom Docker Registry](SETUP.md#custom-docker-registry) for details, including a note on pull-through cache registries (Harbor, Artifactory, Nexus) that require the full registry path in image names.
## Requirements
- Docker and Docker Compose v2
+54
View File
@@ -23,6 +23,22 @@ Key variables:
| `POSTGRES_PASSWORD` | Shared database password (same across all services) |
| `MAS_SECRET_KEY` | 64-char hex encryption key for MAS |
| `LIVEKIT_SECRET` | LiveKit API secret (only if Element Call enabled) |
| `POSTGRES_IMAGE` | PostgreSQL image (default: `postgres:16-alpine`) |
| `SYNAPSE_IMAGE` | Synapse image (default: `matrixdotorg/synapse:latest`) |
| `ELEMENT_IMAGE` | Element Web image (default: `vectorim/element-web:latest`) |
| `ELEMENT_ADMIN_IMAGE` | Element Admin image (default: `oci.element.io/element-admin:latest`) |
| `REDIS_IMAGE` | Redis image (default: `redis:7-alpine`) |
| `MAS_IMAGE` | MAS image (default: `ghcr.io/element-hq/matrix-authentication-service:latest`) |
| `TELEGRAM_IMAGE` | mautrix-telegram image (default: `dock.mau.dev/mautrix/telegram:latest`) |
| `WHATSAPP_IMAGE` | mautrix-whatsapp image (default: `dock.mau.dev/mautrix/whatsapp:latest`) |
| `SIGNAL_IMAGE` | mautrix-signal image (default: `dock.mau.dev/mautrix/signal:latest`) |
| `LIVEKIT_IMAGE` | LiveKit image (default: `livekit/livekit-server:latest`) |
| `LK_JWT_IMAGE` | lk-jwt-service image (default: `ghcr.io/element-hq/lk-jwt-service:latest`) |
| `ELEMENT_CALL_IMAGE` | Element Call image (default: `ghcr.io/element-hq/element-call:latest`) |
| `AUTHELIA_IMAGE` | Authelia image (default: `authelia/authelia:latest`) |
| `CADDY_IMAGE` | Caddy image (default: `caddy:2-alpine`) |
All image variables are optional. If absent, each compose service falls back to the default tag via `${VAR:-default}` syntax, so `docker compose` works without a `.env` file.
### `synapse/data/homeserver.yaml`
@@ -206,6 +222,44 @@ The key name `livekit-key` must match the `LIVEKIT_KEY` value in `docker-compose
---
## Custom Docker Registry
`deploy.sh` prompts for two optional image settings:
**Custom registry prefix** — prepends a registry URL to every image. Useful for air-gapped environments or internal mirrors:
```
Custom Docker registry prefix: myregistry.example.com
```
Results in `.env` entries like:
```
SYNAPSE_IMAGE=myregistry.example.com/matrixdotorg/synapse:latest
REDIS_IMAGE=myregistry.example.com/redis:7-alpine
```
**Hardened images** — uses [dhi.io](https://dhi.io) hardened variants for Redis, PostgreSQL, and Caddy. Takes priority over the custom registry prefix for those three images:
```
REDIS_IMAGE=dhi.io/redis:7
POSTGRES_IMAGE=dhi.io/postgres:16
CADDY_IMAGE=dhi.io/caddy:2
```
### Pull-through cache registries (Harbor, Artifactory, Nexus)
Many enterprise registries act as pull-through caches that mirror images under the **full original registry path**. For example, Harbor's proxy cache serves Docker Hub images as:
```
myregistry.example.com/docker.io/library/redis:7-alpine
myregistry.example.com/docker.io/matrixdotorg/synapse:latest
myregistry.example.com/ghcr.io/element-hq/matrix-authentication-service:latest
```
The custom registry prefix in `deploy.sh` does **not** add the `docker.io/` or other intermediate path components — it produces `myregistry.example.com/redis:7-alpine`. If your mirror requires the full path structure, set the image variables manually in `.env` after running `deploy.sh`, or configure your registry to serve images without the intermediate path (most registries support this as an alias/rewrite).
---
## Docker Compose Profiles
The `docker-compose.yml` uses Docker Compose profiles to make services optional:
+12 -12
View File
@@ -1,7 +1,7 @@
services:
# PostgreSQL Database
postgres:
image: postgres:16-alpine
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
container_name: matrix-postgres
restart: unless-stopped
environment:
@@ -22,7 +22,7 @@ services:
# Matrix Synapse Server
synapse:
image: matrixdotorg/synapse:latest
image: ${SYNAPSE_IMAGE:-matrixdotorg/synapse:latest}
container_name: matrix-synapse
restart: unless-stopped
environment:
@@ -52,7 +52,7 @@ services:
# Element Web Client
element:
image: vectorim/element-web:latest
image: ${ELEMENT_IMAGE:-vectorim/element-web:latest}
container_name: matrix-element
restart: unless-stopped
volumes:
@@ -69,7 +69,7 @@ services:
livekit:
profiles:
- element-call
image: livekit/livekit-server:latest
image: ${LIVEKIT_IMAGE:-livekit/livekit-server:latest}
container_name: matrix-livekit
restart: unless-stopped
ports:
@@ -87,7 +87,7 @@ services:
lk-jwt-service:
profiles:
- element-call
image: ghcr.io/element-hq/lk-jwt-service:latest
image: ${LK_JWT_IMAGE:-ghcr.io/element-hq/lk-jwt-service:latest}
container_name: matrix-lk-jwt
restart: unless-stopped
expose:
@@ -106,7 +106,7 @@ services:
redis:
profiles:
- authelia # Only started when Authelia profile is active
image: redis:7-alpine
image: ${REDIS_IMAGE:-redis:7-alpine}
container_name: matrix-redis
restart: unless-stopped
networks:
@@ -121,7 +121,7 @@ services:
authelia:
profiles:
- authelia # Only started when Authelia profile is active
image: authelia/authelia:latest
image: ${AUTHELIA_IMAGE:-authelia/authelia:latest}
container_name: matrix-authelia
restart: unless-stopped
environment:
@@ -145,7 +145,7 @@ services:
# Matrix Authentication Service (MAS)
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest
image: ${MAS_IMAGE:-ghcr.io/element-hq/matrix-authentication-service:latest}
container_name: matrix-mas
restart: unless-stopped
environment:
@@ -176,7 +176,7 @@ services:
# Caddy Reverse Proxy (HTTPS termination)
caddy:
image: caddy:2-alpine
image: ${CADDY_IMAGE:-caddy:2-alpine}
container_name: matrix-caddy
restart: unless-stopped
ports:
@@ -197,7 +197,7 @@ services:
# mautrix-telegram Bridge
mautrix-telegram:
image: dock.mau.dev/mautrix/telegram:latest
image: ${TELEGRAM_IMAGE:-dock.mau.dev/mautrix/telegram:latest}
container_name: matrix-bridge-telegram
restart: unless-stopped
volumes:
@@ -210,7 +210,7 @@ services:
# mautrix-whatsapp Bridge
mautrix-whatsapp:
image: dock.mau.dev/mautrix/whatsapp:latest
image: ${WHATSAPP_IMAGE:-dock.mau.dev/mautrix/whatsapp:latest}
container_name: matrix-bridge-whatsapp
restart: unless-stopped
volumes:
@@ -223,7 +223,7 @@ services:
# mautrix-signal Bridge
mautrix-signal:
image: dock.mau.dev/mautrix/signal:latest
image: ${SIGNAL_IMAGE:-dock.mau.dev/mautrix/signal:latest}
container_name: matrix-bridge-signal
restart: unless-stopped
volumes:
+92
View File
@@ -180,6 +180,65 @@ else
fi
echo ""
# ============================================================================
# DOCKER REGISTRY AND HARDENED IMAGES
# ============================================================================
echo -e "${CYAN}Docker Image Configuration:${NC}"
echo ""
read -p "Custom Docker registry prefix (leave blank for default): " DOCKER_REGISTRY_INPUT
DOCKER_REGISTRY="${DOCKER_REGISTRY_INPUT%/}" # strip trailing slash
[ -n "$DOCKER_REGISTRY" ] && DOCKER_REGISTRY="${DOCKER_REGISTRY}/"
if [ -n "$DOCKER_REGISTRY" ]; then
echo -e "${GREEN}${NC} Custom registry: ${DOCKER_REGISTRY}"
else
echo -e "${GREEN}${NC} Using default registries"
fi
USE_HARDENED_IMAGES=false
read -p "Use hardened images from dhi.io for Redis/PostgreSQL/Caddy? [y/N]: " yn
[[ "$yn" =~ ^[Yy] ]] && USE_HARDENED_IMAGES=true
if [ "$USE_HARDENED_IMAGES" = true ]; then
echo -e "${GREEN}${NC} Hardened images (dhi.io) enabled for Redis/PostgreSQL/Caddy"
else
echo -e "${GREEN}${NC} Using standard images"
fi
echo ""
# Build image reference helper
build_image() {
local image="$1"
if [ -n "$DOCKER_REGISTRY" ]; then
echo "${DOCKER_REGISTRY}${image}"
else
echo "${image}"
fi
}
# Standard images (respect custom registry)
POSTGRES_IMAGE=$(build_image "postgres:16-alpine")
SYNAPSE_IMAGE=$(build_image "matrixdotorg/synapse:latest")
ELEMENT_IMAGE=$(build_image "vectorim/element-web:latest")
ELEMENT_ADMIN_IMAGE=$(build_image "oci.element.io/element-admin:latest")
MAS_IMAGE=$(build_image "ghcr.io/element-hq/matrix-authentication-service:latest")
TELEGRAM_IMAGE=$(build_image "dock.mau.dev/mautrix/telegram:latest")
WHATSAPP_IMAGE=$(build_image "dock.mau.dev/mautrix/whatsapp:latest")
SIGNAL_IMAGE=$(build_image "dock.mau.dev/mautrix/signal:latest")
LIVEKIT_IMAGE=$(build_image "livekit/livekit-server:latest")
LK_JWT_IMAGE=$(build_image "ghcr.io/element-hq/lk-jwt-service:latest")
ELEMENT_CALL_IMAGE=$(build_image "ghcr.io/element-hq/element-call:latest")
AUTHELIA_IMAGE=$(build_image "authelia/authelia:latest")
# Hardened images take priority for redis/postgres/caddy
if [ "$USE_HARDENED_IMAGES" = true ]; then
REDIS_IMAGE="dhi.io/redis:7"
POSTGRES_IMAGE="dhi.io/postgres:16"
CADDY_IMAGE="dhi.io/caddy:2"
else
REDIS_IMAGE=$(build_image "redis:7-alpine")
CADDY_IMAGE=$(build_image "caddy:2-alpine")
fi
# Function to generate secure random string (32 bytes base64)
generate_secret() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
@@ -403,6 +462,22 @@ MAS_SECRET_KEY=${MAS_SECRET_KEY}
# Timezone
TZ=${TZ:-Europe/Berlin}
# Docker images
POSTGRES_IMAGE=${POSTGRES_IMAGE}
SYNAPSE_IMAGE=${SYNAPSE_IMAGE}
ELEMENT_IMAGE=${ELEMENT_IMAGE}
ELEMENT_ADMIN_IMAGE=${ELEMENT_ADMIN_IMAGE}
REDIS_IMAGE=${REDIS_IMAGE}
MAS_IMAGE=${MAS_IMAGE}
TELEGRAM_IMAGE=${TELEGRAM_IMAGE}
WHATSAPP_IMAGE=${WHATSAPP_IMAGE}
SIGNAL_IMAGE=${SIGNAL_IMAGE}
LIVEKIT_IMAGE=${LIVEKIT_IMAGE}
LK_JWT_IMAGE=${LK_JWT_IMAGE}
ELEMENT_CALL_IMAGE=${ELEMENT_CALL_IMAGE}
AUTHELIA_IMAGE=${AUTHELIA_IMAGE}
CADDY_IMAGE=${CADDY_IMAGE}
EOF
# Add production-specific variables
@@ -1247,6 +1322,23 @@ chmod 755 authelia/config mas/config element/config 2>/dev/null || true
print_status "Permissions fixed"
echo ""
# Image summary
echo -e "${CYAN}Docker images configured:${NC}"
echo -e " Synapse: $SYNAPSE_IMAGE"
echo -e " Postgres: $POSTGRES_IMAGE"
echo -e " Redis: $REDIS_IMAGE"
echo -e " MAS: $MAS_IMAGE"
echo -e " Element: $ELEMENT_IMAGE"
echo -e " Element Admin: $ELEMENT_ADMIN_IMAGE"
echo -e " Authelia: $AUTHELIA_IMAGE"
echo -e " Caddy: $CADDY_IMAGE"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " LiveKit: $LIVEKIT_IMAGE"
echo -e " LK JWT: $LK_JWT_IMAGE"
echo -e " Element Call: $ELEMENT_CALL_IMAGE"
fi
echo ""
# Step 14: Start the stack
echo -e "${BLUE}[14/14] Starting the Matrix stack...${NC}"
print_info "Using compose file: ${COMPOSE_FILE}"
+14 -14
View File
@@ -17,7 +17,7 @@
services:
# PostgreSQL Database
postgres:
image: postgres:16-alpine
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
container_name: matrix-postgres
restart: unless-stopped
environment:
@@ -38,7 +38,7 @@ services:
# Matrix Synapse Server
synapse:
image: matrixdotorg/synapse:latest
image: ${SYNAPSE_IMAGE:-matrixdotorg/synapse:latest}
container_name: matrix-synapse
restart: unless-stopped
environment:
@@ -64,7 +64,7 @@ services:
# Element Web Client
element:
image: vectorim/element-web:latest
image: ${ELEMENT_IMAGE:-vectorim/element-web:latest}
container_name: matrix-element
restart: unless-stopped
volumes:
@@ -79,7 +79,7 @@ services:
# Element Admin - Web UI for managing users/rooms via MAS
element-admin:
image: oci.element.io/element-admin:latest
image: ${ELEMENT_ADMIN_IMAGE:-oci.element.io/element-admin:latest}
container_name: matrix-element-admin
restart: unless-stopped
environment:
@@ -99,7 +99,7 @@ services:
redis:
profiles:
- authelia # Only started when Authelia profile is active
image: redis:7-alpine
image: ${REDIS_IMAGE:-redis:7-alpine}
container_name: matrix-redis
restart: unless-stopped
networks:
@@ -112,7 +112,7 @@ services:
# Matrix Authentication Service (MAS)
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest
image: ${MAS_IMAGE:-ghcr.io/element-hq/matrix-authentication-service:latest}
container_name: matrix-mas
restart: unless-stopped
environment:
@@ -139,7 +139,7 @@ services:
# mautrix-telegram Bridge
mautrix-telegram:
image: dock.mau.dev/mautrix/telegram:latest
image: ${TELEGRAM_IMAGE:-dock.mau.dev/mautrix/telegram:latest}
container_name: matrix-bridge-telegram
restart: unless-stopped
volumes:
@@ -152,7 +152,7 @@ services:
# mautrix-whatsapp Bridge
mautrix-whatsapp:
image: dock.mau.dev/mautrix/whatsapp:latest
image: ${WHATSAPP_IMAGE:-dock.mau.dev/mautrix/whatsapp:latest}
container_name: matrix-bridge-whatsapp
restart: unless-stopped
volumes:
@@ -165,7 +165,7 @@ services:
# mautrix-signal Bridge
mautrix-signal:
image: dock.mau.dev/mautrix/signal:latest
image: ${SIGNAL_IMAGE:-dock.mau.dev/mautrix/signal:latest}
container_name: matrix-bridge-signal
restart: unless-stopped
volumes:
@@ -180,7 +180,7 @@ services:
livekit:
profiles:
- element-call
image: livekit/livekit-server:latest
image: ${LIVEKIT_IMAGE:-livekit/livekit-server:latest}
container_name: matrix-livekit
restart: unless-stopped
ports:
@@ -197,7 +197,7 @@ services:
lk-jwt-service:
profiles:
- element-call
image: ghcr.io/element-hq/lk-jwt-service:latest
image: ${LK_JWT_IMAGE:-ghcr.io/element-hq/lk-jwt-service:latest}
container_name: matrix-lk-jwt
restart: unless-stopped
ports:
@@ -218,7 +218,7 @@ services:
element-call:
profiles:
- element-call
image: ghcr.io/element-hq/element-call:latest
image: ${ELEMENT_CALL_IMAGE:-ghcr.io/element-hq/element-call:latest}
container_name: matrix-element-call
restart: unless-stopped
ports:
@@ -230,7 +230,7 @@ services:
authelia:
profiles:
- authelia # Only started when Authelia profile is active
image: authelia/authelia:latest
image: ${AUTHELIA_IMAGE:-authelia/authelia:latest}
container_name: matrix-authelia
restart: unless-stopped
environment:
@@ -258,7 +258,7 @@ services:
caddy:
profiles:
- single-machine # Only started for single-machine deployments
image: caddy:2-alpine
image: ${CADDY_IMAGE:-caddy:2-alpine}
container_name: matrix-caddy
restart: unless-stopped
ports: