Skip to content

Commit ffbd905

Browse files
authored
feat: load env vars from file or archive at startup (#4053)
1 parent b35db38 commit ffbd905

15 files changed

Lines changed: 250 additions & 3 deletions

File tree

docs/mods-and-plugins/index.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,54 @@ Disabling mods within docker compose files:
156156
mod2.jar
157157
```
158158
159+
### Loading container configuration from a pack
160+
161+
A pack can ship its own container configuration so that the server type, version,
162+
and other variables travel with the pack rather than being declared by the user.
163+
At startup, before `TYPE` is dispatched, the container can load environment
164+
variables from a file on disk, a URL, an entry inside an archive, or from the
165+
`.env` of each `GENERIC_PACK(S)` entry.
166+
167+
- `LOAD_ENV_FROM_GENERIC_PACK`: when `true`, each entry in `GENERIC_PACKS` (after
168+
`GENERIC_PACKS_PREFIX`/`SUFFIX` expansion) is probed for a top-level `.env`
169+
and each one found is sourced in the same order the packs are applied (later
170+
packs override earlier ones, matching the layering of the unpack itself). Packs
171+
without a `.env` are skipped without error. URLs are downloaded into
172+
`/data/packs/` and reused by the regular generic-pack unpack step, so they are
173+
not fetched twice.
174+
- `LOAD_ENV_FROM_FILE`: container path or URL of a shell-style env file (one
175+
`KEY=VALUE` per line). Comments and blank lines are allowed.
176+
- `LOAD_ENV_FROM_ARCHIVE`: container path or URL of a zip/tar archive containing
177+
an env file. The entry is sourced into the environment.
178+
- `LOAD_ENV_FROM_ARCHIVE_ENTRY`: relative path of the env file inside the archive.
179+
Defaults to `.env`.
180+
181+
These can be combined. Load order is: generic packs first, then
182+
`LOAD_ENV_FROM_FILE`, then `LOAD_ENV_FROM_ARCHIVE` — later loads override
183+
earlier ones, and all of them **override** values passed via `docker run -e` (or
184+
compose `environment:`), so the pack's declared values win.
185+
186+
```shell
187+
docker run -d \
188+
-e EULA=TRUE \
189+
-e GENERIC_PACK=https://cdn.example.org/my-pack.zip \
190+
-e LOAD_ENV_FROM_GENERIC_PACK=true \
191+
itzg/minecraft-server
192+
```
193+
194+
Where `my-pack.zip` contains a `.env` at its root such as:
195+
196+
```env
197+
TYPE=FABRIC
198+
VERSION=1.21.1
199+
FABRIC_LOADER_VERSION=0.16.0
200+
```
201+
202+
!!! warning
203+
The env file is sourced by `bash`, so any shell syntax it contains will be
204+
evaluated. Only point these variables at sources you trust. `EULA` cannot be
205+
set this way — it is checked before the env file is loaded.
206+
159207
## Mods/plugins list
160208

161209
You may also download or copy over individual mods/plugins using the `MODS` or `PLUGINS` environment variables. Both are a comma or newline delimited list of

scripts/start-configuration

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,31 @@ fi
152152

153153
cd /data || exit 1
154154

155+
##########################################
156+
# Optionally load environment variables from a file or archive entry,
157+
# allowing packs/artifacts to declare TYPE, VERSION and other settings
158+
# inside-out. Loaded values override anything passed in through docker -e.
159+
# Generic packs are processed first so that LOAD_ENV_FROM_FILE and
160+
# LOAD_ENV_FROM_ARCHIVE can override any values they set.
161+
162+
if isTrue "${LOAD_ENV_FROM_GENERIC_PACK:-false}"; then
163+
if ! loadEnvFromGenericPack; then
164+
exit 1
165+
fi
166+
fi
167+
168+
if [[ ${LOAD_ENV_FROM_FILE:-} ]]; then
169+
if ! loadEnvFromFile "${LOAD_ENV_FROM_FILE}"; then
170+
exit 1
171+
fi
172+
fi
173+
174+
if [[ ${LOAD_ENV_FROM_ARCHIVE:-} ]]; then
175+
if ! loadEnvFromArchive "${LOAD_ENV_FROM_ARCHIVE}" "${LOAD_ENV_FROM_ARCHIVE_ENTRY:-.env}"; then
176+
exit 1
177+
fi
178+
fi
179+
155180
export DECLARED_TYPE=${TYPE^^}
156181
export DECLARED_VERSION="$VERSION"
157182

scripts/start-utils

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,117 @@ function applyResultsFile() {
2626
set +a
2727
}
2828

29+
function loadEnvFromFile() {
30+
local source=${1?Missing required source argument}
31+
local downloaded=
32+
33+
if isURL "$source"; then
34+
mkdir -p /data/.tmp
35+
downloaded=$(mktemp -p /data/.tmp)
36+
log "Downloading env file from $source"
37+
if ! get -o "$downloaded" "$source"; then
38+
logError "Failed to download env file from $source"
39+
rm -f "$downloaded"
40+
return 1
41+
fi
42+
log "Loading env vars from $source"
43+
applyResultsFile "$downloaded"
44+
rm -f "$downloaded"
45+
elif [ -f "$source" ]; then
46+
log "Loading env vars from $source"
47+
applyResultsFile "$source"
48+
else
49+
logError "Env file not found: $source"
50+
return 1
51+
fi
52+
}
53+
54+
function loadEnvFromArchive() {
55+
local source=${1?Missing required source argument}
56+
local entry=${2:-.env}
57+
local archive=
58+
local downloaded=
59+
local tmpdir
60+
local rc=0
61+
62+
mkdir -p /data/.tmp
63+
64+
if isURL "$source"; then
65+
downloaded=$(mktemp -p /data/.tmp)
66+
log "Downloading archive from $source"
67+
if ! get -o "$downloaded" "$source"; then
68+
logError "Failed to download archive from $source"
69+
rm -f "$downloaded"
70+
return 1
71+
fi
72+
archive=$downloaded
73+
elif [ -f "$source" ]; then
74+
archive=$source
75+
else
76+
logError "Archive not found: $source"
77+
return 1
78+
fi
79+
80+
tmpdir=$(mktemp -d -p /data/.tmp)
81+
if extract "$archive" "$tmpdir" "$entry" && [ -f "$tmpdir/$entry" ]; then
82+
log "Loading env vars from '$entry' in $source"
83+
applyResultsFile "$tmpdir/$entry"
84+
else
85+
logError "Failed to load env entry '$entry' from $source"
86+
rc=1
87+
fi
88+
89+
rm -rf "$tmpdir"
90+
[[ -n "$downloaded" ]] && rm -f "$downloaded"
91+
return $rc
92+
}
93+
94+
function loadEnvFromGenericPack() {
95+
: "${GENERIC_PACKS:=${GENERIC_PACK:-}}"
96+
: "${GENERIC_PACKS_PREFIX:=}"
97+
: "${GENERIC_PACKS_SUFFIX:=}"
98+
99+
if [[ -z "${GENERIC_PACKS}" ]]; then
100+
logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but GENERIC_PACK(S) is empty"
101+
return 0
102+
fi
103+
104+
mkdir -p /data/.tmp
105+
IFS=',' read -ra packs <<< "${GENERIC_PACKS}"
106+
local loaded=0
107+
local pack packEntry packFile tmpdir
108+
for packEntry in "${packs[@]}"; do
109+
pack="${GENERIC_PACKS_PREFIX}${packEntry}${GENERIC_PACKS_SUFFIX}"
110+
if isURL "$pack"; then
111+
mkdir -p /data/packs
112+
if ! packFile=$(get -o /data/packs --output-filename --skip-up-to-date "$pack"); then
113+
logError "Failed to download generic pack $pack"
114+
return 1
115+
fi
116+
else
117+
packFile=$pack
118+
fi
119+
120+
if [[ ! -f "$packFile" ]]; then
121+
logError "Generic pack not found: $packFile"
122+
return 1
123+
fi
124+
125+
tmpdir=$(mktemp -d -p /data/.tmp)
126+
# Packs without a .env are valid — silently skip; the unpack step still applies them.
127+
if extract "$packFile" "$tmpdir" .env 2>/dev/null && [ -f "$tmpdir/.env" ]; then
128+
log "Loading env vars from .env in $pack"
129+
applyResultsFile "$tmpdir/.env"
130+
loaded=$((loaded + 1))
131+
fi
132+
rm -rf "$tmpdir"
133+
done
134+
135+
if (( loaded == 0 )); then
136+
logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but no pack in GENERIC_PACK(S) contained a .env"
137+
fi
138+
}
139+
29140
function join_by() {
30141
local d=$1
31142
shift
@@ -445,17 +556,19 @@ function isType() {
445556
function extract() {
446557
src=${1?}
447558
destDir=${2?}
559+
shift 2
560+
# remaining args are paths within the archive to extract; if none, extract everything
448561

449562
type=$(file -b --mime-type "${src}")
450563
case "${type}" in
451564
application/zip)
452-
unzip -o -q -d "${destDir}" "${src}"
565+
unzip -o -q -d "${destDir}" "${src}" "$@"
453566
;;
454567
application/x-tar | application/gzip | application/x-gzip | application/x-bzip2)
455-
tar -C "${destDir}" -xf "${src}"
568+
tar -C "${destDir}" -xf "${src}" "$@"
456569
;;
457570
application/zstd | application/x-zstd)
458-
tar -C "${destDir}" --use-compress-program=unzstd -xf "${src}"
571+
tar -C "${destDir}" --use-compress-program=unzstd -xf "${src}" "$@"
459572
;;
460573
*)
461574
logError "Unsupported archive type: $type"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
mc:
3+
image: ${IMAGE_TO_TEST:-itzg/minecraft-server}
4+
environment:
5+
EULA: "true"
6+
SETUP_ONLY: "true"
7+
LOAD_ENV_FROM_ARCHIVE: /test/load-env.zip
8+
MOTD: from-compose
9+
LOG_TIMESTAMP: "true"
10+
DEBUG: "true"
11+
# the following are only used to speed up test execution
12+
TYPE: CUSTOM
13+
CUSTOM_SERVER: /servers/fake.jar
14+
VERSION: 1.18.1
15+
volumes:
16+
- ./data:/data
17+
- ./load-env.zip:/test/load-env.zip
18+
- ./fake.jar:/servers/fake.jar

tests/setuponlytests/load-env-from-archive/fake.jar

Whitespace-only changes.
221 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mc-image-helper assert propertyEquals --file=server.properties --property=motd --expect=from-archive
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
mc:
3+
image: ${IMAGE_TO_TEST:-itzg/minecraft-server}
4+
environment:
5+
EULA: "true"
6+
SETUP_ONLY: "true"
7+
LOAD_ENV_FROM_FILE: /test/load-env.env
8+
MOTD: from-compose
9+
LOG_TIMESTAMP: "true"
10+
DEBUG: "true"
11+
# the following are only used to speed up test execution
12+
TYPE: CUSTOM
13+
CUSTOM_SERVER: /servers/fake.jar
14+
VERSION: 1.18.1
15+
volumes:
16+
- ./data:/data
17+
- ./load-env.env:/test/load-env.env
18+
- ./fake.jar:/servers/fake.jar

tests/setuponlytests/load-env-from-file/fake.jar

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Loaded by LOAD_ENV_FROM_FILE during start-configuration
2+
MOTD=from-env-file

0 commit comments

Comments
 (0)