Skip to content

Commit d0dcb73

Browse files
authored
Merge pull request #3 from linuxserver/multi-host
2 parents 87fdf6a + 1360701 commit d0dcb73

4 files changed

Lines changed: 150 additions & 13 deletions

File tree

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The architectures supported by this image are:
5252

5353
## Application Setup
5454

55-
You can specify mods to download via the `DOCKER_MODS` environment variable like any other container, or allow discovery through docker by mounting the docker socket into the container (or configuring a suitable alternative endpoint via DOCKER_HOST).
55+
You can specify mods to download via the `DOCKER_MODS` environment variable like any other container, or allow discovery through docker by mounting the docker socket into the container (or configuring a suitable alternative endpoint via the built-in `DOCKER_HOST` environment variable). Whichever option you choose the appropriate `DOCKER_MODS` environment variable must still be present on the containers that need to install them.
5656

5757
The Modmanager container will download all needed mods on startup and then check for updates every 6 hours; if you're using docker discovery it will automatically pick up any new mods.
5858

@@ -62,9 +62,48 @@ If a mod requires additional packages to be installed, each container will still
6262

6363
Note that the Modmanager container itself does not support applying mods *or* custom files/services.
6464

65+
**Modmanager is only supported for use with Linuxserver images built after 2025-01-01, while it may work with 3rd party containers using our images as a base we will not provide support for them.**
66+
6567
### Security considerations
6668

67-
Mapping `docker.sock` is a potential security liability because docker has root access on the host and any process that has full access to `docker.sock` would also have root access on the host. Docker api has no built-in way to set limitations on access, however, you can use a proxy for the `docker.sock` via a solution like [our docker socket proxy](https://github.com/linuxserver/docker-socket-proxy), which adds the ability to limit access. Then you would just set `DOCKER_HOST=` environment variable to point to the proxy address.
69+
Mapping `docker.sock` is a potential security liability because docker has root access on the host and any process that has full access to `docker.sock` would therefore also have root access on the host. The docker API has no built-in way to set limitations on access, however, you can use a proxy for `docker.sock` via a solution like [our docker socket proxy](https://github.com/linuxserver/docker-socket-proxy), which adds the ability to limit API access to specific endpoints.
70+
71+
### Multiple Hosts
72+
73+
>[!NOTE]
74+
>Make sure you fully understand what you're doing before you try and set this up as there are lots of ways it can go wrong if you're just guessing.
75+
76+
Modmanager can query & download mods for remote hosts, as well as the one on which it is installed. At a very basic level if you're just using the `DOCKER_MODS` env and not docker discovery, simply mount the `/modcache` folder on your remote host(s), ensuring it is mapped for all participating containers.
77+
78+
If you are using docker discovery, our only supported means for connecting to remote hosts is [our socket proxy container](https://github.com/linuxserver/docker-socket-proxy/). Run an instance on each remote host:
79+
80+
>[!WARNING]
81+
>DO NOT expose a socket proxy to your LAN if it allows any write operations (`POST=1`, `ALLOW_RESTART=1`, etc) or exposes any API elements that are not absolutely necessary. NEVER expose a socket proxy to your WAN.
82+
83+
```yml
84+
modmanager-dockerproxy:
85+
image: lscr.io/linuxserver/socket-proxy:latest
86+
container_name: modmanager-dockerproxy
87+
environment:
88+
- CONTAINERS=1
89+
- POST=0
90+
volumes:
91+
- /var/run/docker.sock:/var/run/docker.sock:ro
92+
tmpfs:
93+
- /run:exec
94+
ports:
95+
- 2375:2375
96+
restart: unless-stopped
97+
read_only: true
98+
```
99+
100+
And then add it to the `DOCKER_MODS_EXTRA_HOSTS` env using the full protocol and port, separating multiple servers with a pipe (`|`), e.g.
101+
102+
```yaml
103+
- DOCKER_MODS_EXTRA_HOSTS=tcp://host1.example.com:2375|tcp://host2.example.com:2375|tcp://192.168.0.5:2375
104+
```
105+
106+
As above you will need to mount the `/modcache` folder on your remote host(s), ensuring it is mapped for all participating containers.
68107

69108
## Usage
70109

@@ -84,6 +123,7 @@ services:
84123
environment:
85124
- DOCKER_MODS= `#optional`
86125
- DOCKER_HOST= `#optional`
126+
- DOCKER_MODS_EXTRA_HOSTS= `#optional`
87127
volumes:
88128
- /path/to/modcache:/modcache
89129
- /var/run/docker.sock:/var/run/docker.sock:ro `#optional`
@@ -97,6 +137,7 @@ docker run -d \
97137
--name=modmanager \
98138
-e DOCKER_MODS= `#optional` \
99139
-e DOCKER_HOST= `#optional` \
140+
-e DOCKER_MODS_EXTRA_HOSTS= `#optional` \
100141
-v /path/to/modcache:/modcache \
101142
-v /var/run/docker.sock:/var/run/docker.sock:ro `#optional` \
102143
--restart unless-stopped \
@@ -111,6 +152,7 @@ Containers are configured using parameters passed at runtime (such as those abov
111152
| :----: | --- |
112153
| `-e DOCKER_MODS=` | Pipe-delimited (`\|`) list of mods to download |
113154
| `-e DOCKER_HOST=` | Specify the docker endpoint to use if not using the docker.sock |
155+
| `-e DOCKER_MODS_EXTRA_HOSTS=` | Pipe-delimited (`\|`) list of additional hosts to query & download mods for. See app setup section for details. |
114156
| `-v /modcache` | Modmanager mod storage. |
115157
| `-v /var/run/docker.sock:ro` | Mount the host docker socket into the container. |
116158

@@ -234,4 +276,5 @@ Once registered you can define the dockerfile to use with `-f Dockerfile.aarch64
234276

235277
## Versions
236278

279+
* **05.01.25:** - Support multiple hosts.
237280
* **22.12.24:** - Initial Release.

readme-vars.yml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ full_custom_readme: |
5656
5757
## Application Setup
5858
59-
You can specify mods to download via the `DOCKER_MODS` environment variable like any other container, or allow discovery through docker by mounting the docker socket into the container (or configuring a suitable alternative endpoint via DOCKER_HOST).
59+
You can specify mods to download via the `DOCKER_MODS` environment variable like any other container, or allow discovery through docker by mounting the docker socket into the container (or configuring a suitable alternative endpoint via the built-in `DOCKER_HOST` environment variable). Whichever option you choose the appropriate `DOCKER_MODS` environment variable must still be present on the containers that need to install them.
6060
6161
The Modmanager container will download all needed mods on startup and then check for updates every 6 hours; if you're using docker discovery it will automatically pick up any new mods.
6262
@@ -66,9 +66,48 @@ full_custom_readme: |
6666
6767
Note that the Modmanager container itself does not support applying mods *or* custom files/services.
6868
69+
**Modmanager is only supported for use with Linuxserver images built after 2025-01-01, while it may work with 3rd party containers using our images as a base we will not provide support for them.**
70+
6971
### Security considerations
7072
71-
Mapping `docker.sock` is a potential security liability because docker has root access on the host and any process that has full access to `docker.sock` would also have root access on the host. Docker api has no built-in way to set limitations on access, however, you can use a proxy for the `docker.sock` via a solution like [our docker socket proxy](https://github.com/linuxserver/docker-socket-proxy), which adds the ability to limit access. Then you would just set `DOCKER_HOST=` environment variable to point to the proxy address.
73+
Mapping `docker.sock` is a potential security liability because docker has root access on the host and any process that has full access to `docker.sock` would therefore also have root access on the host. The docker API has no built-in way to set limitations on access, however, you can use a proxy for `docker.sock` via a solution like [our docker socket proxy](https://github.com/linuxserver/docker-socket-proxy), which adds the ability to limit API access to specific endpoints.
74+
75+
### Multiple Hosts
76+
77+
>[!NOTE]
78+
>Make sure you fully understand what you're doing before you try and set this up as there are lots of ways it can go wrong if you're just guessing.
79+
80+
Modmanager can query & download mods for remote hosts, as well as the one on which it is installed. At a very basic level if you're just using the `DOCKER_MODS` env and not docker discovery, simply mount the `/modcache` folder on your remote host(s), ensuring it is mapped for all participating containers.
81+
82+
If you are using docker discovery, our only supported means for connecting to remote hosts is [our socket proxy container](https://github.com/linuxserver/docker-socket-proxy/). Run an instance on each remote host:
83+
84+
>[!WARNING]
85+
>DO NOT expose a socket proxy to your LAN if it allows any write operations (`POST=1`, `ALLOW_RESTART=1`, etc) or exposes any API elements that are not absolutely necessary. NEVER expose a socket proxy to your WAN.
86+
87+
```yml
88+
modmanager-dockerproxy:
89+
image: lscr.io/linuxserver/socket-proxy:latest
90+
container_name: modmanager-dockerproxy
91+
environment:
92+
- CONTAINERS=1
93+
- POST=0
94+
volumes:
95+
- /var/run/docker.sock:/var/run/docker.sock:ro
96+
tmpfs:
97+
- /run:exec
98+
ports:
99+
- 2375:2375
100+
restart: unless-stopped
101+
read_only: true
102+
```
103+
104+
And then add it to the `DOCKER_MODS_EXTRA_HOSTS` env using the full protocol and port, separating multiple servers with a pipe (`|`), e.g.
105+
106+
```yaml
107+
- DOCKER_MODS_EXTRA_HOSTS=tcp://host1.example.com:2375|tcp://host2.example.com:2375|tcp://192.168.0.5:2375
108+
```
109+
110+
As above you will need to mount the `/modcache` folder on your remote host(s), ensuring it is mapped for all participating containers.
72111
73112
## Usage
74113
@@ -88,6 +127,7 @@ full_custom_readme: |
88127
environment:
89128
- DOCKER_MODS= `#optional`
90129
- DOCKER_HOST= `#optional`
130+
- DOCKER_MODS_EXTRA_HOSTS= `#optional`
91131
volumes:
92132
- /path/to/modcache:/modcache
93133
- /var/run/docker.sock:/var/run/docker.sock:ro `#optional`
@@ -101,6 +141,7 @@ full_custom_readme: |
101141
--name=modmanager \
102142
-e DOCKER_MODS= `#optional` \
103143
-e DOCKER_HOST= `#optional` \
144+
-e DOCKER_MODS_EXTRA_HOSTS= `#optional` \
104145
-v /path/to/modcache:/modcache \
105146
-v /var/run/docker.sock:/var/run/docker.sock:ro `#optional` \
106147
--restart unless-stopped \
@@ -115,6 +156,7 @@ full_custom_readme: |
115156
| :----: | --- |
116157
| `-e DOCKER_MODS=` | Pipe-delimited (`\|`) list of mods to download |
117158
| `-e DOCKER_HOST=` | Specify the docker endpoint to use if not using the docker.sock |
159+
| `-e DOCKER_MODS_EXTRA_HOSTS=` | Pipe-delimited (`\|`) list of additional hosts to query & download mods for. See app setup section for details. |
118160
| `-v /modcache` | Modmanager mod storage. |
119161
| `-v /var/run/docker.sock:ro` | Mount the host docker socket into the container. |
120162
@@ -238,6 +280,7 @@ full_custom_readme: |
238280
239281
## Versions
240282
283+
* **05.01.25:** - Support multiple hosts.
241284
* **22.12.24:** - Initial Release.
242285
243286
{%- endraw %}

root/app/update-mods.sh

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,57 @@
11
#!/usr/bin/with-contenv bash
22
# shellcheck shell=bash
33

4-
# Main script loop
5-
if [[ -e "/var/run/docker.sock" ]] || [[ -n "${DOCKER_HOST}" ]]; then
4+
find_docker_mods() {
65
# Mods provided via Docker
7-
echo -e "[mod-init] Searching all containers for DOCKER_MODS..."
8-
for CONTAINER in $(docker ps -q); do
9-
CONTAINER_MODS=$(docker inspect "${CONTAINER}" | jq -r '.[].Config.Env | to_entries | map(select(.value | match("DOCKER_MODS="))) | .[].value')
10-
CONTAINER_NAME=$(docker inspect "${CONTAINER}" | jq -r .[].Name | cut -d '/' -f2)
6+
if [[ "${2}" != "default" ]]; then
7+
local MOD_STATE="(${2})"
8+
docker context create "${2}" --docker "host=${1}" >/dev/null 2>&1
9+
fi
10+
docker --context "${2}" ps -q >/dev/null 2>&1 || local DOCKER_MOD_CONTEXT_FAIL=true
11+
if [[ "${DOCKER_MOD_CONTEXT_FAIL}" == "true" ]]; then
12+
echo "[mod-init] (ERROR) Cannot connect to the Docker daemon at ${2}, skipping host"
13+
return
14+
fi
15+
echo -e "[mod-init] ${MOD_STATE:+${MOD_STATE} }Searching all containers in the ${2} context for DOCKER_MODS..."
16+
for CONTAINER in $(docker --context "${2}" ps -q); do
17+
CONTAINER_MODS=$(docker --context "${2}" inspect "${CONTAINER}" | jq -r '.[].Config.Env | to_entries | map(select(.value | match("DOCKER_MODS="))) | .[].value')
18+
CONTAINER_NAME=$(docker --context "${2}" inspect "${CONTAINER}" | jq -r .[].Name | cut -d '/' -f2)
1119
if [[ -n ${CONTAINER_MODS} ]]; then
1220
CONTAINER_MODS=$(awk -F '=' '{print $2}' <<< "${CONTAINER_MODS}")
1321
for CONTAINER_MOD in $(tr '|' '\n' <<< "${CONTAINER_MODS}"); do
1422
if [[ "${DOCKER_MODS}" =~ ${CONTAINER_MOD} ]]; then
15-
echo -e "[mod-init] ${CONTAINER_MOD} already in mod list, skipping"
23+
echo -e "[mod-init] ${MOD_STATE:+${MOD_STATE} }${CONTAINER_MOD} already in mod list, skipping"
1624
else
17-
echo -e "[mod-init] Found new mod ${CONTAINER_MOD} for container ${CONTAINER_NAME}"
25+
echo -e "[mod-init] ${MOD_STATE:+${MOD_STATE} }Found new mod ${CONTAINER_MOD} for container ${CONTAINER_NAME}"
1826
DOCKER_MODS="${DOCKER_MODS}|${CONTAINER_MOD}"
1927
DOCKER_MODS="${DOCKER_MODS#|}"
2028
fi
2129
done
2230
fi
2331
done
32+
if [[ "${2}" != "default" ]]; then
33+
docker context rm "${2}" >/dev/null
34+
fi
35+
}
36+
37+
# Main script loop
38+
39+
# Reset DOCKER_MODS to whatever value the user passed into the container at creation time
40+
DOCKER_MODS="${DOCKER_MODS_STATIC}"
41+
42+
echo -e ""
43+
echo -e "[mod-init] Running check for new mods and updates."
44+
45+
if [[ -e "/var/run/docker.sock" ]] || [[ -n "${DOCKER_HOST}" ]]; then
46+
find_docker_mods "${DOCKER_HOST:-docker.sock}" "default"
47+
fi
48+
49+
if [[ -n "${DOCKER_MODS_EXTRA_HOSTS}" ]]; then
50+
for DOCKER_MOD_CONTEXT in $(echo "${DOCKER_MODS_EXTRA_HOSTS}" | tr '|' '\n'); do
51+
DOCKER_MOD_CONTEXT_NAME="${DOCKER_MOD_CONTEXT##*//}"
52+
DOCKER_MOD_CONTEXT_NAME="${DOCKER_MOD_CONTEXT_NAME%%:*}"
53+
find_docker_mods "${DOCKER_MOD_CONTEXT}" "${DOCKER_MOD_CONTEXT_NAME}"
54+
done
2455
fi
2556

2657
if [[ -n "${DOCKER_MODS}" ]]; then

root/etc/s6-overlay/s6-rc.d/init-modmanager-config/run

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ CRON_MINS=$((0 + RANDOM % 59))
55

66
sed -i "s/@@MINUTES@@/${CRON_MINS}/" /etc/crontabs/root
77

8-
echo "[mod-init] Mod updates will run every 6 hours at ${CRON_MINS} minutes past the hour"
8+
if [[ $(date "+%-H") == 0 && $(date "+%-M") -lt ${CRON_MINS} ]]; then
9+
NEXT_HOUR=0
10+
elif [[ $(date "+%-H") == 6 && $(date "+%-M") -lt ${CRON_MINS} ]]; then
11+
NEXT_HOUR=6
12+
elif [[ $(date "+%-H") == 12 && $(date "+%-M") -lt ${CRON_MINS} ]]; then
13+
NEXT_HOUR=12
14+
elif [[ $(date "+%-H") == 18 && $(date "+%-M") -lt ${CRON_MINS} ]]; then
15+
NEXT_HOUR=18
16+
elif [[ $(date "+%-H") -ge 0 && $(date "+%-H") -le 5 ]]; then
17+
NEXT_HOUR=6
18+
elif [[ $(date "+%-H") -ge 6 && $(date "+%-H") -le 11 ]]; then
19+
NEXT_HOUR=12
20+
elif [[ $(date "+%-H") -ge 12 && $(date "+%-H") -le 17 ]]; then
21+
NEXT_HOUR=18
22+
elif [[ $(date "+%-H") -ge 18 && $(date "+%-H") -le 23 ]]; then
23+
NEXT_HOUR=0
24+
fi
25+
26+
echo "[mod-init] Mod updates will run every 6 hours at ${CRON_MINS} minutes past the hour. Next update will be at $(date -d${NEXT_HOUR}:${CRON_MINS} '+%H:%M')."
27+
28+
printf %s "${DOCKER_MODS}" > /run/s6/container_environment/DOCKER_MODS_STATIC
929

1030
/app/update-mods.sh

0 commit comments

Comments
 (0)