mirror of
https://github.com/linuxserver/docker-mods.git
synced 2026-03-23 00:05:28 +08:00
Initial
This commit is contained in:
parent
6360fcf7e3
commit
f9a8b10b30
BIN
.assets/loading-page.png
Normal file
BIN
.assets/loading-page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -2,7 +2,7 @@
|
||||
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="username"
|
||||
LABEL maintainer="quietsy"
|
||||
|
||||
# copy local files
|
||||
COPY root/ /
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
## Buildstage ##
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.20 AS buildstage
|
||||
|
||||
RUN \
|
||||
echo "**** install packages ****" && \
|
||||
apk add --no-cache \
|
||||
curl && \
|
||||
echo "**** grab rclone ****" && \
|
||||
mkdir -p /root-layer && \
|
||||
if [ $(uname -m) = "x86_64" ]; then \
|
||||
echo "Downloading x86_64 tarball" && \
|
||||
curl -o \
|
||||
/root-layer/rclone.deb -L \
|
||||
"https://downloads.rclone.org/v1.47.0/rclone-v1.47.0-linux-amd64.deb"; \
|
||||
elif [ $(uname -m) = "aarch64" ]; then \
|
||||
echo "Downloading aarch64 tarball" && \
|
||||
curl -o \
|
||||
/root-layer/rclone.deb -L \
|
||||
"https://downloads.rclone.org/v1.47.0/rclone-v1.47.0-linux-arm64.deb"; \
|
||||
fi && \
|
||||
|
||||
# copy local files
|
||||
COPY root/ /root-layer/
|
||||
|
||||
## Single layer deployed image ##
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="username"
|
||||
|
||||
# Add files from buildstage
|
||||
COPY --from=buildstage /root-layer/ /
|
||||
89
README.md
89
README.md
@ -1,25 +1,78 @@
|
||||
# Rsync - Docker mod for openssh-server
|
||||
# On-demand - Docker mod for SWAG
|
||||
|
||||
This mod adds rsync to openssh-server, to be installed/updated during container start.
|
||||
This mod gives SWAG the ability to start containers on-demand when accessed through SWAG and stop them after a period of inactivity. It takes a few seconds for containers to start on-demand, you'll need to refresh the tab.
|
||||
|
||||
In openssh-server docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:openssh-server-rsync`
|
||||
## Setup:
|
||||
- In SWAG's docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:swag-ondemand` and either add a volume mapping for `/var/run/docker.sock:/var/run/docker.sock:ro`, or set an environment var `DOCKER_HOST=remoteaddress` (read the security considerations below).
|
||||
- Add the label `swag_ondemand=enable` to on-demand containers.
|
||||
```yaml
|
||||
somecontainer:
|
||||
container_name: somecontainer
|
||||
...
|
||||
labels:
|
||||
- swag_ondemand=enable
|
||||
```
|
||||
- Replace the following line in `/config/nginx/nginx.conf`:
|
||||
```nginx
|
||||
access_log /config/log/nginx/access.log;
|
||||
```
|
||||
With:
|
||||
```nginx
|
||||
log_format main '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request_method $scheme://$host$request_uri $server_protocol" '
|
||||
'$status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
access_log /config/log/nginx/access.log main;
|
||||
```
|
||||
- *Optional* - In SWAG's docker arguments, set an environment variable `SWAG_ONDEMAND_STOP_THRESHOLD` to override the period of inactivity in seconds before stopping the container. Defaults to `600` which is 10 minutes.
|
||||
```yaml
|
||||
swag:
|
||||
container_name: swag
|
||||
...
|
||||
environment:
|
||||
- SWAG_ONDEMAND_STOP_THRESHOLD=600
|
||||
```
|
||||
### Loading Page:
|
||||
|
||||
If adding multiple mods, enter them in an array separated by `|`, such as `DOCKER_MODS=linuxserver/mods:openssh-server-rsync|linuxserver/mods:openssh-server-mod2`
|
||||

|
||||
|
||||
# Mod creation instructions
|
||||
Instead of showing a 502 error page, it can display a loading page and auto-refresh once the container is up.
|
||||
|
||||
Add the following `include` to each proxy-conf where you wish to show the loading page inside the `server` section:
|
||||
```nginx
|
||||
server {
|
||||
...
|
||||
include /config/nginx/ondemand.conf;
|
||||
...
|
||||
```
|
||||
### Labels:
|
||||
- `swag_ondemand=enable` - required for on-demand.
|
||||
- `swag_ondemand_urls=https://wake.domain.com,https://app.domain.com/up` - *optional* - overrides the monitored URLs for starting the container on-demand. Defaults to `https://somecontainer.,http://somecontainer.`.
|
||||
|
||||
* Fork the repo, create a new branch based on the branch `template`.
|
||||
* Edit the `Dockerfile` for the mod. `Dockerfile.complex` is only an example and included for reference; it should be deleted when done.
|
||||
* Inspect the `root` folder contents. Edit, add and remove as necessary.
|
||||
* After all init scripts and services are created, run `find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print -exec chmod +x {} +` to fix permissions.
|
||||
* Edit this readme with pertinent info, delete these instructions.
|
||||
* Finally edit the `.github/workflows/BuildImage.yml`. Customize the vars for `BASEIMAGE` and `MODNAME`. Set the versioning logic and `MULTI_ARCH` if needed.
|
||||
* Ask the team to create a new branch named `<baseimagename>-<modname>`. Baseimage should be the name of the image the mod will be applied to. The new branch will be based on the `template` branch.
|
||||
* Submit PR against the branch created by the team.
|
||||
### URLs:
|
||||
- Accessed URLs need to start with one of `swag_ondemand_urls` to be matched, for example, setting `swag_ondemand_urls=https://plex.` will apply to `https://plex.domain.com` and `https://plex.domain.com/something`.
|
||||
- `swag_ondemand_urls` default to `https://somecontainer.,http://somecontainer.`, for example `https://plex.,http://plex.`.
|
||||
- `swag_ondemand_urls` don't need to be valid, it will work as long as it reaches swag and gets logged by nginx under `/config/log/nginx/access.log`.
|
||||
- The same URL can be set on multiple containers and all of them will be started when accessing that URL.
|
||||
|
||||
## Security Consideration:
|
||||
Mapping the `docker.sock`, especially in a publicly accessible container is a security liability. Since this mod only needs read-only access to the docker api, the recommended method is to proxy the `docker.sock` via a solution like [our docker socket proxy](https://github.com/linuxserver/docker-socket-proxy), limit the access, and set `DOCKER_HOST=` to point to the proxy address.
|
||||
|
||||
## Tips and tricks
|
||||
|
||||
* Some images have helpers built in, these images are currently:
|
||||
* [Openvscode-server](https://github.com/linuxserver/docker-openvscode-server/pull/10/files)
|
||||
* [Code-server](https://github.com/linuxserver/docker-code-server/pull/95)
|
||||
Here's a sample compose yaml snippet for `linuxserver/docker-socket-proxy`:
|
||||
```yaml
|
||||
socket-proxy:
|
||||
image: lscr.io/linuxserver/socket-proxy:latest
|
||||
container_name: socket-proxy
|
||||
environment:
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
- CONTAINERS=1
|
||||
- POST=0
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run
|
||||
```
|
||||
Then the env var in SWAG can be set as `DOCKER_HOST=tcp://socket-proxy:2375`. This will allow docker in SWAG to be able to start/stop existing containers, but it won't be allowed to spin up new containers.
|
||||
|
||||
134
root/app/swag-ondemand.py
Normal file
134
root/app/swag-ondemand.py
Normal file
@ -0,0 +1,134 @@
|
||||
from datetime import datetime
|
||||
import docker
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
ACCESS_LOG_FILE = "/config/log/nginx/access.log"
|
||||
LOG_FILE = "/config/log/ondemand/ondemand.log"
|
||||
STOP_THRESHOLD = int(os.environ.get("SWAG_ONDEMAND_STOP_THRESHOLD", "600"))
|
||||
|
||||
last_accessed_urls = set()
|
||||
last_accessed_urls_lock = threading.Lock()
|
||||
|
||||
class ContainerThread(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.ondemand_containers = {}
|
||||
try:
|
||||
self.docker_client = docker.from_env()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
def process_containers(self):
|
||||
containers = self.docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] })
|
||||
container_names = {container.name for container in containers}
|
||||
|
||||
for container_name in list(self.ondemand_containers.keys()):
|
||||
if container_name in container_names:
|
||||
continue
|
||||
self.ondemand_containers.pop(container_name)
|
||||
logging.info(f"Stopped monitoring {container_name}")
|
||||
|
||||
for container in containers:
|
||||
container_urls = container.labels.get("swag_ondemand_urls", f"https://{container.name}.,http://{container.name}.")
|
||||
if container.name not in self.ondemand_containers.keys():
|
||||
last_accessed = datetime.now()
|
||||
logging.info(f"Started monitoring {container.name}")
|
||||
else:
|
||||
last_accessed = self.ondemand_containers[container.name]["last_accessed"]
|
||||
self.ondemand_containers[container.name] = { "status": container.status, "urls": container_urls, "last_accessed": last_accessed }
|
||||
|
||||
def stop_containers(self):
|
||||
for container_name in self.ondemand_containers.keys():
|
||||
if self.ondemand_containers[container_name]["status"] != "running":
|
||||
continue
|
||||
inactive_seconds = (datetime.now() - self.ondemand_containers[container_name]["last_accessed"]).total_seconds()
|
||||
if inactive_seconds < STOP_THRESHOLD:
|
||||
continue
|
||||
self.docker_client.containers.get(container_name).stop()
|
||||
logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity")
|
||||
|
||||
def start_containers(self):
|
||||
with last_accessed_urls_lock:
|
||||
last_accessed_urls_combined = ",".join(last_accessed_urls)
|
||||
last_accessed_urls.clear()
|
||||
|
||||
for container_name in self.ondemand_containers.keys():
|
||||
accessed = False
|
||||
for ondemand_url in self.ondemand_containers[container_name]["urls"].split(","):
|
||||
if ondemand_url not in last_accessed_urls_combined:
|
||||
continue
|
||||
self.ondemand_containers[container_name]["last_accessed"] = datetime.now()
|
||||
accessed = True
|
||||
if not accessed or self.ondemand_containers[container_name]["status"] == "running":
|
||||
continue
|
||||
self.docker_client.containers.get(container_name).start()
|
||||
logging.info(f"Started {container_name}")
|
||||
self.ondemand_containers[container_name]["status"] = "running"
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
self.process_containers()
|
||||
self.start_containers()
|
||||
self.stop_containers()
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
class LogReaderThread(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
|
||||
def tail(self, f):
|
||||
f.seek(0,2)
|
||||
inode = os.fstat(f.fileno()).st_ino
|
||||
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
time.sleep(1)
|
||||
if os.stat(ACCESS_LOG_FILE).st_ino != inode:
|
||||
f.close()
|
||||
f = open(ACCESS_LOG_FILE, 'r')
|
||||
inode = os.fstat(f.fileno()).st_ino
|
||||
continue
|
||||
yield line
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
if not os.path.exists(ACCESS_LOG_FILE):
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
logfile = open(ACCESS_LOG_FILE, "r")
|
||||
for line in self.tail(logfile):
|
||||
for part in line.split():
|
||||
if not part.startswith("http"):
|
||||
continue
|
||||
with last_accessed_urls_lock:
|
||||
last_accessed_urls.add(part)
|
||||
break
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
time.sleep(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||
logging.basicConfig(filename=LOG_FILE,
|
||||
filemode='a',
|
||||
format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.INFO)
|
||||
logging.info("Starting swag-ondemand...")
|
||||
|
||||
ContainerThread().start()
|
||||
LogReaderThread().start()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
19
root/defaults/ondemand.conf
Normal file
19
root/defaults/ondemand.conf
Normal file
@ -0,0 +1,19 @@
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 = @waking_up;
|
||||
location @waking_up {
|
||||
add_header Retry-After 5 always;
|
||||
default_type text/html;
|
||||
return 502 '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Waking Up...</title>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<style>body{font-family:sans-serif;text-align:center;padding-top:50px;background-color:#303030;color:#ffffff;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Application is sleeping</h1>
|
||||
<p>Please wait while it wakes up...</p>
|
||||
<p>This page will refresh automatically.</p>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is the init file used for adding os or pip packages to install lists.
|
||||
# It takes advantage of the built-in init-mods-package-install init script that comes with the baseimages.
|
||||
# If using this, we need to make sure we set this init as a dependency of init-mods-package-install so this one runs first
|
||||
|
||||
if ! command -v apprise; then
|
||||
echo "**** Adding apprise and its deps to package install lists ****"
|
||||
echo "apprise" >> /mod-pip-packages-to-install.list
|
||||
## Ubuntu
|
||||
if [ -f /usr/bin/apt ]; then
|
||||
echo "\
|
||||
python3 \
|
||||
python3-pip \
|
||||
runc" >> /mod-repo-packages-to-install.list
|
||||
fi
|
||||
# Alpine
|
||||
if [ -f /sbin/apk ]; then
|
||||
echo "\
|
||||
cargo \
|
||||
libffi-dev \
|
||||
openssl-dev \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3 \
|
||||
py3-pip" >> /mod-repo-packages-to-install.list
|
||||
fi
|
||||
else
|
||||
echo "**** apprise already installed, skipping ****"
|
||||
fi
|
||||
@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-add-package/run
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is an install script that is designed to run after init-mods-package-install
|
||||
# so it can take advantage of packages installed
|
||||
# init-mods-end depends on this script so that later init and services wait until this script exits
|
||||
|
||||
echo "**** Setting up apprise ****"
|
||||
apprise blah blah
|
||||
@ -1 +0,0 @@
|
||||
oneshot
|
||||
@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-install/run
|
||||
24
root/etc/s6-overlay/s6-rc.d/init-mod-swag-ondemand-setup/run
Executable file
24
root/etc/s6-overlay/s6-rc.d/init-mod-swag-ondemand-setup/run
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "Applying the swag-ondemand mod..."
|
||||
|
||||
if [ ! -S /var/run/docker.sock ] && [ -z "$DOCKER_HOST" ]; then
|
||||
echo "**** Docker not set up properly, skipping swag-ondemand ****"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '**** Checking if docker-py is already installed ****'
|
||||
if ! pip list 2>&1 | grep -q "docker"; then
|
||||
echo "**** Adding docker-py to package install lists ****"
|
||||
echo "\
|
||||
docker" >> /mod-pip-packages-to-install.list
|
||||
else
|
||||
echo "**** docker-py already installed, skipping ****"
|
||||
fi
|
||||
|
||||
if [ ! -f /config/nginx/ondemand.conf ]; then
|
||||
cp /defaults/ondemand.conf /config/nginx/ondemand.conf
|
||||
lsiown -R abc:abc /config/nginx/ondemand.conf
|
||||
fi
|
||||
|
||||
echo "Applied the swag-ondemand mod"
|
||||
@ -0,0 +1 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-swag-ondemand-setup/run
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is an example service that would run for the mod
|
||||
# It depends on init-services, the baseimage hook for start of all longrun services
|
||||
|
||||
exec \
|
||||
s6-setuidgid abc run my app
|
||||
3
root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run
Executable file
3
root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
exec s6-setuidgid abc python3 /app/swag-ondemand.py
|
||||
Loading…
x
Reference in New Issue
Block a user