Migrate docudjeex to Docus v4 with EN/FR content
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
title: My nonsense
|
||||
icon: i-noto-test-tube
|
||||
@@ -0,0 +1,2 @@
|
||||
title: Python
|
||||
icon: i-lucide-file-code-2
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Nvidia Stock Bot
|
||||
description: A Python bot that monitors GPU stock availability in real time and sends Discord alerts — built during the RTX 5000 series launch shortage.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# 🤖 Nvidia Stock Bot
|
||||
---
|
||||
|
||||
For the past four years, the electronics hardware shortage has been relentless. Graphics cards are no exception. In 2020, I had to wait two months to get my RTX 3080. To manage it, I joined [JV Hardware](https://discord.gg/gxffg3GA96), where a small group of geeks had set up a bot that pinged users when GPUs became available.
|
||||
|
||||
Four years later and with 5,000 members on the server, the RTX 5000 series is being released. Yet, no working stock bot seems to exist. Not to mention a certain “influencer” who charges users for access to a bot that doesn’t even work. He manually copies alerts from other servers like ours, which have already solved the issue.
|
||||
|
||||
Anyway, eager to get an RTX 5090 for my AI-dedicated machine, I decided it was time to dive into Python—with a little help from ChatGPT. Along with another member, KevOut, who helped guide me through the APIs and initial architecture, I ended up building a clean and functional bot that sends different kinds of Discord alerts—all deployable in a simple Docker container.
|
||||
|
||||
After many setbacks, I went from this:
|
||||
|
||||

|
||||
|
||||
To this:
|
||||
|
||||

|
||||
|
||||
And more recently :
|
||||
|
||||

|
||||
|
||||
And I was also lucky enough to be referenced in the famous [selfhost newsletter](https://selfh.st/weekly/2025-07-11/) !
|
||||
|
||||
More info directly on the repo:
|
||||
|
||||
::card{title="🐋 __Nvidia Stock Bot__" to="https://git.djeex.fr/Djeex/nvidia-stock-bot" target="_blank"}
|
||||
Nvidia GPU stock alert bot
|
||||
::
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Adguard CIDRE
|
||||
description: A Python script to sync AdGuard Home CIDR allowlists automatically, securing your self-hosted DNS server exposed to the internet.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# 🤖 Adguard CIDRE Sync
|
||||
---
|
||||
|
||||
Adguard Home is a fantastic solution for DNS-level ad blocking and rewriting requests—perfect for removing ISP DNS trackers or intrusive ads.
|
||||
|
||||
It works great locally, but if you want all your devices (even on the go) to benefit, you’ll need to expose Adguard to the internet. Unfortunately, that means anyone can use it, potentially overloading your €1/month remote VPS.
|
||||
|
||||
Adguard allows whitelisting or blacklisting clients. The problem? To whitelist a client, you need their IP—but for mobile phones, that IP changes often. Instead of trying to whitelist ever-changing IPs, the better approach is to block broader IP ranges by region.
|
||||
|
||||
CIDRE is a tool that syncs geo-targeted IP ranges with firewalls. Instead of running CIDRE with a full firewall stack on the remote server, I figured I could just import those regularly updated IP ranges into Adguard’s blocklist.
|
||||
|
||||
Thus, Adguard CIDRE Sync was born: a container that syncs Adguard’s blocklist with CIDRE’s updated IP ranges on a schedule of your choosing.
|
||||
|
||||
The idea is to:
|
||||
|
||||
- Backup Adguard’s config file on first run (original untouched version saved)
|
||||
- Download selected country IP ranges via an environment variable
|
||||
- Let you manually add custom IPs via a file
|
||||
- Concatenate, backup the config again (as the updated version), and inject the list into the correct blocklist section
|
||||
- Reload Adguard by restarting the container (using Docker socket proxy for limited permissions)
|
||||
|
||||
All fully autonomous, with frequency set via environment variable in the `docker-compose` config.
|
||||
|
||||
More info directly on the repo:
|
||||
|
||||
::card{title="🐋 __Adguard CIDRE Sync__" to="https://git.djeex.fr/Djeex/adguard-cidre" target="_blank"}
|
||||
Adguard blocklist sync bot
|
||||
::
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Lumeex
|
||||
description: Lumeex is a static photo gallery site generator built with Python — minimalist, lightweight, and fully customizable without a CMS.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
<div align="center">
|
||||
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/logo.svg" alt="Lumeex Screenshot" width="300"/>
|
||||
</div>
|
||||
<div align="center">
|
||||
<p>Yet another minimalist, lightweight photo gallery static site generator.</p>
|
||||
</div>
|
||||
<div align="center">
|
||||
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Amateur photographer that I am, I spent several weeks looking for a framework with a photo gallery that could outshine Instagram. I wanted something that showcased the photos rather than the author, and that made every visit unique by loading the images in random order—while still allowing filtering and sorting by tag or combinations of tags.
|
||||
|
||||
In the end, I found nothing that did exactly what I wanted. And when something came close, it was always through heavy, bloated CMS platforms. So I decided to make a static site by hand, the old-school way, with Notepad++. Being fairly comfortable with HTML/CSS and a bit of JavaScript, I quickly came up with something nice during my vacation, between beach sessions. After all, a good craftsman should have good tools—and there’s no better tool than one you make yourself.
|
||||
|
||||
Then I thought it might be a good idea to automate certain tasks—like generating favicon formats, resizing and converting images, creating the gallery automatically instead of entering everything by hand, and generating `robots.txt` and `sitemap` files… so I turned back to Python.
|
||||
|
||||
Eventually, after getting good results, I figured I might as well go all the way: build a complete framework for generating a static site photo gallery, where all you need to do is fill in your site’s information in a config file and tweak the visuals a bit—without touching the code.
|
||||
|
||||
That’s how **Lum[eex]{style="color: #1ad6ff"}** was born.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
### Et voilà!
|
||||
|
||||
:::div{class="relative"}
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||
:::
|
||||
|
||||
::card-group
|
||||
::card{icon="i-noto-open-book" title="Documentation" to="https://lumeex.djeex.fr" target="_blank"}
|
||||
Check out the doc
|
||||
::
|
||||
|
||||
::card{icon="i-simple-icons-gitea" title="Repository" to="https://git.djeex.fr/Djeex/lumeex" target="_blank"}
|
||||
See the repo
|
||||
::
|
||||
|
||||
::card{icon="i-fluent-color-design-ideas-48" title="Demo" to="https://modern.djeex.fr" target="_blank"}
|
||||
Explore the demo
|
||||
::
|
||||
::
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Instameex
|
||||
description: Instameex is a Docker-based tool to merge SDR and HDR photo exports into a proper gain-map JPEG ready for Instagram HDR upload.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
<div align="center">
|
||||
<img src="https://git.djeex.fr/Djeex/instameex/raw/branch/main/src/assets/img/logo-long.svg" alt="Instameex Screenshot" width="300"/>
|
||||
</div>
|
||||
<div align="center">
|
||||
<p>Mix your SDR and HDR exports into an Instagram-ready HDR photo.</p>
|
||||
</div>
|
||||
<div align="center">
|
||||
<img src="https://git.djeex.fr/Djeex/instameex/raw/branch/main/illustration/instameex-illustration.png" width="640" alt="Instameex Screenshot" />
|
||||
</div>
|
||||
---
|
||||
|
||||
Nothing is more frustrating than Instagram's HDR handling. It compresses and destroys gain maps, and the slightest change in aspect ratio or size simply strips HDR out entirely. As for Lightroom, its "SDR preview" system is frankly unacceptable, it makes it impossible to get consistent results. Until now, posting on Instagram meant choosing between decent SDR with broken HDR, or the other way around.
|
||||
|
||||
Why not simply edit your SDR file to perfection on one side, your HDR file on the other, and then recalculate a gain map from those two perfect files?
|
||||
A few pioneers have already gone down that road, notably with an [Adobe Lightroom Classic](https://github.com/karachungen/lightroom-plugin-export-hdr) plugin. Judge me if you want, but I only use Lightroom CC, which does not support plugins.
|
||||
|
||||
I drew inspiration from a [fork of the original project](https://github.com/kostis-kounadis/instagram-hdr-assembler), the one that eventually became the LrC plugin, to build a frontend that can be easily deployed with Docker. Let's be honest: it was also a great excuse to put my Claude Code subscription to the test. And I have to say, watching it spin up its own environments, run end-to-end tests, self-correct its code, and write detailed summaries is genuinely impressive. I still reviewed everything myself, don't worry. I also learned a great deal about HDR fundamentals, gain maps, HLG/PQ tone curves, color spaces, and more.
|
||||
|
||||
In short, here is what my workflow now looks like for posting on Instagram:
|
||||
|
||||

|
||||
|
||||
Allow me to introduce **Instam[eex]{style="color: #1ad6ff"}**
|
||||
|
||||
---
|
||||
### And here is the result
|
||||
|
||||
:::div{class="relative"}
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||
:::
|
||||
|
||||
::card-group
|
||||
::card{title="🐋 __Instameex__" to="https://git.djeex.fr/Djeex/instameex" target="_blank"}
|
||||
Open the repository
|
||||
::
|
||||
|
||||
::card{title="🌍 __Online version__" to="https://instameex.djeex.fr" target="_blank"}
|
||||
Convert online
|
||||
::
|
||||
::
|
||||
@@ -0,0 +1,2 @@
|
||||
title: Bash
|
||||
icon: i-lucide-file-terminal
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Servarr corrector
|
||||
description: A bash script to detect and fix duplicate media files in Sonarr and Radarr libraries by replacing copies with hardlinks to reclaim disk space.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# Servarr duplicates corrector
|
||||
---
|
||||
|
||||
Six months after downloading terabytes of media, I realized that Sonarr and Radarr were copying them into my Plex library instead of creating hardlinks. This happens due to a counterintuitive mechanism: if you mount multiple folders in Sonarr/Radarr, it sees them as different filesystems and thus cannot create hardlinks. That’s why you should mount only one parent folder containing all child folders (like `downloads`, `movies`, `tvseries` inside a `media` parent folder).
|
||||
|
||||
So I restructured my directories, manually updated every path in Qbittorrent, Plex, and others. The last challenge was finding a way to detect existing duplicates, delete them, and automatically create hardlinks instead—to save space.
|
||||
|
||||
My directory structure:
|
||||
|
||||
```sh
|
||||
.
|
||||
└── media
|
||||
├── seedbox
|
||||
├── radarr
|
||||
│ └── tv-radarr
|
||||
├── movies
|
||||
└── tvseries
|
||||
```
|
||||
|
||||
The originals are in `seedbox` and must not be modified to keep seeding. The copies (duplicates) are in `movies` and `tvseries`. To complicate things, there are also unique originals in `movies` and `tvseries`. And within those, there can be subfolders, sub-subfolders, etc.
|
||||
|
||||
So the idea is to:
|
||||
|
||||
- list the originals in seedbox
|
||||
- list files in movies and tvseries
|
||||
- compare both lists and isolate duplicates
|
||||
- delete the duplicates
|
||||
- hardlink the originals to the deleted duplicate paths
|
||||
|
||||
Yes, I asked ChatGPT and Qwen3 (which I host on a dedicated AI machine). Naturally, they suggested tools like rfind, rdfind, dupes, rdupes, rmlint... But hashing 30TB of media would take days, so I gave up quickly.
|
||||
|
||||
In the end, I only needed to find `.mkv` files, and duplicates have the exact same name as the originals, which simplifies things a lot. A simple Bash script would do the job.
|
||||
|
||||
Spare you the endless Q&A with ChatGPT—I was disappointed. Qwen3 was much cleaner. ChatGPT kept pushing awk-based solutions, which fail on paths with spaces. With Qwen’s help and dropping awk, the results improved significantly.
|
||||
|
||||
To test, I first asked for a script that only lists and compares:
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
|
||||
# Create an associative array to store duplicates
|
||||
declare -A seen
|
||||
|
||||
# Find all .mkv files only (exclude directories)
|
||||
find /media/seedbox /media/movies /media/tvseries -type f -name "*.mkv" -print0 | \
|
||||
while IFS= read -r -d '' file; do
|
||||
# Get the file's inode and name
|
||||
inode=$(stat --format="%i" "$file")
|
||||
filename=$(basename "$file")
|
||||
|
||||
# If the filename has been seen before
|
||||
if [[ -n "${seen[$filename]}" ]]; then
|
||||
# Check if the inode is different from the previous one
|
||||
if [[ "${seen[$filename]}" != "$inode" ]]; then
|
||||
# Output the duplicates with full paths
|
||||
echo "Duplicates for \"$filename\":"
|
||||
echo "${seen["$filename"]} ${seen["$filename:full_path"]}"
|
||||
echo "$inode $file"
|
||||
echo
|
||||
fi
|
||||
else
|
||||
seen[$filename]="$inode"
|
||||
seen["$filename:full_path"]="$file"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
This gave me outputs like:
|
||||
|
||||
```
|
||||
Duplicates for "episode1.mkv":
|
||||
1234567 /media/seedbox/sonarr/Serie 1/Season1/episode1.mkv
|
||||
2345678 /media/tvseries/Serie 1/Season1/episode1.mkv
|
||||
```
|
||||
|
||||
With `awk`, it would’ve stopped at `/media/seedbox/sonarr/Serie`. I’m far from an expert, but Qwen3 performed better and explained everything clearly.
|
||||
|
||||
Once I verified the output, I asked for a complete script: compare, delete duplicates, create hardlinks.
|
||||
|
||||
Again, ChatGPT disappointed. Despite my requests, it created hardlinks *before* deleting the duplicates—effectively linking and then deleting the link (though the original is kept). Not helpful.
|
||||
|
||||
Quick stopover to Qwen3, RTX 5090 in overdrive, and bam—much better result. Yes, it kept ChatGPT-style emojis, but here it is:
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔍 Step 1: Indexing original files in /media/seedbox..."
|
||||
declare -A seen
|
||||
|
||||
# Index all .mkv files in seedbox
|
||||
while IFS= read -r -d '' file; do
|
||||
filename=$(basename "$file")
|
||||
seen["$filename"]="$file"
|
||||
done < <(find /media/seedbox -type f -name "*.mkv" -print0)
|
||||
|
||||
echo "📦 Step 2: Automatically replacing duplicates..."
|
||||
total_doublons=0
|
||||
total_ko_saved=0
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
filename=$(basename "$file")
|
||||
original="${seen[$filename]}"
|
||||
|
||||
if [[ -n "$original" && "$original" != "$file" ]]; then
|
||||
inode_orig=$(stat -c %i "$original")
|
||||
inode_dupe=$(stat -c %i "$file")
|
||||
|
||||
if [[ "$inode_orig" != "$inode_dupe" ]]; then
|
||||
size_kb=$(du -k "$file" | cut -f1)
|
||||
echo "🔁 Replacing:"
|
||||
echo " Duplicate : $file"
|
||||
echo " Original : $original"
|
||||
echo " Size : ${size_kb} KB"
|
||||
|
||||
rm "$file" && ln "$original" "$file" && echo "✅ Hardlink created."
|
||||
|
||||
total_doublons=$((total_doublons + 1))
|
||||
total_ko_saved=$((total_ko_saved + size_kb))
|
||||
fi
|
||||
fi
|
||||
done < <(find /media/movies /media/tvseries -type f -name "*.mkv" -print0)
|
||||
|
||||
echo ""
|
||||
echo "🧾 Summary:"
|
||||
echo " 🔗 Duplicates replaced by hardlink: $total_doublons"
|
||||
echo " 💾 Approx. disk space saved: ${total_ko_saved} KB (~$((total_ko_saved / 1024)) MB)"
|
||||
echo "✅ Done."
|
||||
```
|
||||
|
||||
So, in conclusion, I:
|
||||
|
||||
- Learned many Bash subtleties
|
||||
- Learned never to blindly copy-paste a ChatGPT script without understanding and dry-running it
|
||||
- Learned that Qwen on a RTX 5090 is more coherent than ChatGPT-4o on server farms (not even mentioning “normal” ChatGPT)
|
||||
- Learned that even with 100TB of storage, monitoring it would’ve alerted me much earlier to the 12TB of duplicates lying around
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: LUKS Backup
|
||||
description: A bash script to automatically dump LUKS headers from all encrypted disks, identify them by serial number, and store them in an encrypted archive.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# Backup of LUKS Headers for Encrypted Disks/Volumes
|
||||
---
|
||||
|
||||
I recently realized that having just the password is not enough to unlock a LUKS volume after a failure or corruption. I learned how to dump the LUKS headers from disks/volumes and to use the serial numbers along with partition names to accurately identify which header corresponds to which disk/partition (I have 10 of them!).
|
||||
|
||||
After struggling to do this manually, I asked Qwen3 (an LLM running on my RTX 5090) to create a script that automates the listing and identification of disks, dumps the headers, and stores them in an encrypted archive ready to be backed up on my backup server.
|
||||
|
||||
This script:
|
||||
|
||||
* Lists and identifies disks with their serial numbers
|
||||
* Lists partitions
|
||||
* Dumps headers into a secured folder under `/root`
|
||||
* Creates a temporary archive
|
||||
* Prompts for a password
|
||||
* Encrypts the archive with that password
|
||||
* Deletes the unencrypted archive
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
|
||||
# Directory where LUKS headers will be backed up
|
||||
DEST="/root/luks-headers-backup"
|
||||
mkdir -p "$DEST"
|
||||
|
||||
echo "🔍 Searching for LUKS containers on all partitions..."
|
||||
|
||||
# Loop through all possible disk partitions (including NVMe and SATA)
|
||||
for part in /dev/sd? /dev/sd?? /dev/nvme?n?p?; do
|
||||
# Skip if the device doesn't exist
|
||||
if [ ! -b "$part" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if the partition is a LUKS encrypted volume
|
||||
if cryptsetup isLuks "$part"; then
|
||||
# Find the parent disk device (e.g. nvme0n1p4 → nvme0n1)
|
||||
disk=$(lsblk -no pkname "$part" | head -n 1)
|
||||
full_disk="/dev/$disk"
|
||||
|
||||
# Get the serial number of the parent disk
|
||||
SERIAL=$(udevadm info --query=all --name="$full_disk" | grep ID_SERIAL= | cut -d= -f2)
|
||||
if [ -z "$SERIAL" ]; then
|
||||
SERIAL="unknown"
|
||||
fi
|
||||
|
||||
# Extract the partition name (e.g. nvme0n1p4)
|
||||
PART_NAME=$(basename "$part")
|
||||
|
||||
# Build the output filename with partition name and disk serial
|
||||
OUTPUT="$DEST/luks-header-${PART_NAME}__${SERIAL}.img"
|
||||
|
||||
echo "🔐 Backing up LUKS header of $part (Serial: $SERIAL)..."
|
||||
|
||||
# Backup the LUKS header to the output file
|
||||
cryptsetup luksHeaderBackup "$part" --header-backup-file "$OUTPUT"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo "✅ Backup successful → $OUTPUT"
|
||||
else
|
||||
echo "❌ Backup failed for $part"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Create a timestamped compressed tar archive of all header backups
|
||||
ARCHIVE_NAME="/root/luks-headers-$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||
echo "📦 Creating archive $ARCHIVE_NAME..."
|
||||
tar -czf "$ARCHIVE_NAME" -C "$DEST" .
|
||||
|
||||
# Encrypt the archive symmetrically using GPG with AES256 cipher
|
||||
echo "🔐 Encrypting the archive with GPG..."
|
||||
gpg --symmetric --cipher-algo AES256 "$ARCHIVE_NAME"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo "✅ Encrypted archive created: ${ARCHIVE_NAME}.gpg"
|
||||
# Remove the unencrypted archive for security
|
||||
rm -f "$ARCHIVE_NAME"
|
||||
else
|
||||
echo "❌ Encryption failed"
|
||||
fi
|
||||
```
|
||||
|
||||
**Don’t forget to back up `/etc/fstab` and `/etc/crypttab` as well!**
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Socat Proxy
|
||||
description: Use socat to proxy the Docker socket through Docker Socket Proxy, allowing Beszel to collect container stats without exposing the full Docker socket.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# Socat Proxy
|
||||
---
|
||||
|
||||
This project addresses a common use case:
|
||||
|
||||
- I have [Beszel](https://beszel.dev/), a monitoring container running in host mode, which requires access to the Docker socket to collect container statistics.
|
||||
- To avoid exposing the Docker socket fully to Beszel, I use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy), a container that sits between the Docker socket and the consuming container. It filters requests by setting appropriate permissions, preventing full exposure of the Docker socket.
|
||||
|
||||
The problem arises when **Beszel** runs in host mode. In that case, it must connect directly to **Docker Socket Proxy** on a host port, meaning the proxy’s port is exposed. This allows any container or application on the host to access it and use the Docker socket.
|
||||
|
||||
This is where [Socat Proxy](https://git.djeex.fr/Djeex/socat-proxy) comes in. It is a container that:
|
||||
|
||||
- Creates a UNIX socket
|
||||
- Listens on this socket
|
||||
- Forwards requests to Docker Socket Proxy and back
|
||||
- Replaces the real Docker socket by exposing the proxy socket in the target container via a bind mount (in this case, Beszel)
|
||||
|
||||
With this setup, Docker Socket Proxy communicates with Socat Proxy in their isolated bridge network, while the UNIX socket bind-mounted on the host has restricted permissions, preventing access from other containers or applications.
|
||||
|
||||
In short:
|
||||
|
||||

|
||||
|
||||
For example, with Beszel, the configuration would look like this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
socat-proxy:
|
||||
image: git.djeex.fr/djeex/socat-proxy:latest
|
||||
container_name: socat-proxy-beszel
|
||||
environment:
|
||||
|
||||
- TARGET_HOST=${TARGET_HOST}
|
||||
- TARGET_PORT=${TARGET_PORT}
|
||||
- UNIX_SOCKET_PATH=${UNIX_SOCKET_PATH}
|
||||
- HOST_SOCKET_PATH=${HOST_SOCKET_PATH}
|
||||
- UNIX_SOCKET_NAME=${UNIX_SOCKET_NAME}
|
||||
volumes:
|
||||
|
||||
- ${HOST_SOCKET_PATH}:${UNIX_SOCKET_PATH}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
||||
- ${TARGET_HOST}
|
||||
|
||||
socket-proxy:
|
||||
image: lscr.io/linuxserver/socket-proxy:latest
|
||||
container_name: ${TARGET_HOST}
|
||||
security_opt:
|
||||
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
|
||||
- CONTAINERS=1
|
||||
- INFO=1
|
||||
volumes:
|
||||
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs:
|
||||
|
||||
- /run
|
||||
|
||||
beszel-agent:
|
||||
image: henrygd/beszel-agent:latest
|
||||
container_name: beszel-agent
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
security_opt:
|
||||
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
|
||||
- ${HOST_SOCKET_PATH}/${UNIX_SOCKET_NAME}:/var/run/docker.sock:ro
|
||||
environment:
|
||||
|
||||
- #... your Beszel environment variables
|
||||
depends_on:
|
||||
|
||||
- socat-proxy
|
||||
```
|
||||
|
||||
More information is available on the repository:
|
||||
|
||||
::card{title="🐋 **Socat Proxy**" to="https://git.djeex.fr/Djeex/socat-proxy" target="_blank"}
|
||||
A lightweight bind-mount socket proxy
|
||||
::
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: HotDisk
|
||||
description: A bash script that monitors hard drive temperatures and automatically shuts down the server when disks stay above a safe threshold for too long.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# HotDisk
|
||||
---
|
||||
|
||||
When you have a NAS with several drives sitting in a laundry room, temperatures can quickly rise.
|
||||
Hard drives are very sensitive to heat and can suffer serious damage if they exceed a certain temperature threshold for too long.
|
||||
After a particularly hot summer that caused a few cold sweats while monitoring my drives’ temperatures, I started looking for a way to automatically shut down the server when disk temperatures stay above their safe limit for an extended period.
|
||||
|
||||
Since I couldn’t find a convincing solution, I decided to build my own.
|
||||
|
||||
- The script reads SMART temperature data from all SATA drives every minute.
|
||||
- It counts the number of consecutive minutes the temperature stays above or below the threshold.
|
||||
- It sends Discord notifications if the threshold is exceeded or when the temperature cools down.
|
||||
- It triggers a system shutdown if the temperature stays above the limit for the configured duration.
|
||||
- It logs all temperatures and counter states, and automatically rotates log files.
|
||||
|
||||
While I was at it, I also added an installation script that installs the main script, makes it executable, creates a systemd service and timer, and enables them automatically.
|
||||
The installer also lets you configure various parameters:
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
|-----------------------|------------------------------------------------------------------------------|-----------------------------------------------|
|
||||
| `MAX_TEMP` | Maximum allowed temperature (°C) before the shutdown countdown starts | `60` |
|
||||
| `HOT_DURATION` | Consecutive minutes above `MAX_TEMP` before shutdown | `5` |
|
||||
| `COOL_RESET_DURATION` | Consecutive minutes below `MAX_TEMP` to reset all counters | `5` |
|
||||
| `LOG_FILE` | Path to the main log file | `/var/log/hdd_temp_monitor.log` |
|
||||
| `LOG_ROTATE_COUNT` | Number of log files to keep | `7` |
|
||||
| `LOG_ROTATE_PERIOD` | Log rotation period (`daily` or `weekly`) | `daily` |
|
||||
| `DISCORD_WEBHOOK` | Discord webhook URL for notifications | _Required_ |
|
||||
|
||||
It also runs another script that configures **logrotate** with the parameters defined above.
|
||||
Finally, the installer can even be executed directly via a simple `curl` command followed by one last setup script — perfect for the laziest of us.
|
||||
|
||||
I also had to handle several tricky cases: running as root without sudo, using sudo directly, running as a non-sudo user, missing dependencies, permission issues, file creation errors, disk data reading errors, and more.
|
||||
|
||||
Concurrent access to the status file also had to be managed carefully.
|
||||
|
||||
More details are available directly on the repository:
|
||||
|
||||
::card{title="📜 __HotDisk__" to="https://git.djeex.fr/Djeex/hotdisk" target="_blank"}
|
||||
Keep your drives cool!
|
||||
::
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Backrest Docker Stop
|
||||
description: A bash script that stops Docker containers before a Backrest backup runs and restarts them after — ensuring safe database backups without complex dumps.
|
||||
---
|
||||
|
||||
|
||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
||||
# Backrest Docker Stop
|
||||
---
|
||||
|
||||
[Backrest](https://github.com/garethgeorge/backrest) is a fantastic backup tool. In the case of [Serveex](https://docu.djeex.fr/en/serveex/introduction), most of the data that needs to be backed up consists of containers, and those containers often include databases.
|
||||
|
||||
The problem? You can’t safely back up a database while it’s running. There are plenty of complex solutions involving database dumps, but often the simplest method is to stop the containers, perform the backup, and then restart them.
|
||||
|
||||
**Backrest** doesn’t natively provide this functionality, but it does allow the execution of custom scripts triggered by events, for example, at the start and end of a backup plan. Our goal is to stop the containers whose databases need to be backed up when the backup plan starts, and restart them when the backup plan finishes.To achieve this, we’ll need a small Bash script and a secure connection between Backrest and the Docker socket, to enable the following sequence:
|
||||
|
||||
- The backup plan starts
|
||||
- The event triggers the execution of a custom script
|
||||
- The script contacts Docker and retrieves a list of containers labeled `backrest.backup.stop=true`
|
||||
- It stops those containers
|
||||
- The backup plan completes
|
||||
- The event triggers another custom script
|
||||
- The script contacts Docker again, retrieves the same list, and restarts those containers
|
||||
|
||||
## Securely Connecting Backrest and Docker
|
||||
|
||||
To allow **Backrest** to communicate securely with Docker, we’ll use [Docker Socket Proxy](https://github.com/linuxserver/docker-socket-proxy).
|
||||
This avoids exposing the full Docker socket and grants only the necessary permissions.
|
||||
Here’s an example Docker stack:
|
||||
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:latest
|
||||
container_name: backrest
|
||||
hostname: backrest
|
||||
security_opt:
|
||||
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
|
||||
- ... # your volumes
|
||||
environment:
|
||||
|
||||
- ... # your environment variables
|
||||
- DOCKER_HOST=tcp://socket-proxy-backrest:2375
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
- ... # your ports
|
||||
depends_on:
|
||||
|
||||
- socket-proxy
|
||||
|
||||
socket-proxy:
|
||||
image: lscr.io/linuxserver/socket-proxy:latest
|
||||
container_name: socket-proxy-backrest
|
||||
security_opt:
|
||||
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
|
||||
- CONTAINERS=1
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
volumes:
|
||||
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs:
|
||||
|
||||
- /run
|
||||
```
|
||||
|
||||
With this setup, Backrest can communicate with Docker safely and securely.
|
||||
|
||||
## The Scripts
|
||||
|
||||
Below are the scripts to use for **Backrest**’s *start* and *end* backup events.
|
||||
|
||||
::code-group
|
||||
```sh [Stop]
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BACKUP_LABEL="backrest.backup.stop=true"
|
||||
BACKUP_CONTAINERS=$(docker ps -aqf "label=$BACKUP_LABEL")
|
||||
for BC in $BACKUP_CONTAINERS
|
||||
do
|
||||
docker stop "$BC"
|
||||
done
|
||||
sleep 10
|
||||
```
|
||||
|
||||
```sh [Start]
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BACKUP_LABEL="backrest.backup.stop=true"
|
||||
BACKUP_CONTAINERS=$(docker ps -aqf "label=$BACKUP_LABEL")
|
||||
for BC in $BACKUP_CONTAINERS
|
||||
do
|
||||
docker start "$BC"
|
||||
done
|
||||
sleep 10
|
||||
```
|
||||
::
|
||||
|
||||
## The Label
|
||||
|
||||
Once the scripts are in place and configured for the proper **Backrest** hooks, you just need to add the label `backrest.backup.stop=true` to the `compose.yaml` files of the containers that should stop and restart during backups:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
your_service:
|
||||
...
|
||||
labels:
|
||||
|
||||
- backrest.backup.stop=true
|
||||
```
|
||||
|
||||
And that’s it!
|
||||
At the next backup, all containers with the correct label will automatically stop during the backup and restart once it’s finished.
|
||||
Reference in New Issue
Block a user