Galera Server Setup on HostHatch

Set up the Galera cluster using HostHatch’s private networking rather than WireGuard for the Galera replication traffic. HostHatch provides an isolated private VLAN/VXLAN between VMs in the same location, with no bandwidth charge, and Debian 13 is supported for the private interface configuration. (HostHatch Docs)

I’ll walk you through it one step at a time. Don’t do all three at once yet—we’ll configure node 1 first, verify it, then node 2 and node 3.

Target setup

Let’s assume:

db1.example.com    192.168.10.1
db2.example.com    192.168.10.2
db3.example.com    192.168.10.3

And:

db1 = Galera node 1
db2 = Galera node 2
db3 = Galera node 3

Your actual private IPs can be different; those are just examples.

The eventual cluster will be:

                 Private Network
                       │
          ┌────────────┼────────────┐
          │            │            │
       db1            db2          db3
    192.168.10.1  192.168.10.2  192.168.10.3
          │            │            │
          └────────────┼────────────┘
                       │
                     Galera
                       │
                  3-node quorum

Step 1 — Create the three VPSs

I’d start with your inexpensive plan:

Each:

  • 2 AMD EPYC Milan cores
  • 4 GB RAM
  • 20 GB NVMe
  • Debian 13
  • Los Angeles

You can upgrade them later.

In the HostHatch control panel, enable Private Networking for the VPSs. HostHatch requires at least two active VMs in the same location. When enabled, each VM gets an additional private interface. (HostHatch Docs)

For Debian 13, HostHatch’s documented private interface configuration is via ifupdown. The private interface is normally eth1, but we’ll verify rather than assume. (HostHatch Docs)


Step 2 — Configure the private network

SSH into db1.

Run:

ip -br link

and:

ip -o link | grep 00:22

You should find the HostHatch private interface. HostHatch says the private interface MAC begins with 00:22. (HostHatch Docs)

Then:

ip -br addr

Initially you’ll probably see something like:

eth0    UP    <public IP>
enp2s0   DOWN

We’ll assign:

db1 → 192.168.10.1/24
db2 → 192.168.10.2/24
db3 → 192.168.10.3/24

Do not configure a gateway on this interface. HostHatch specifically warns that the private interface cannot reach external networks and should not have a default gateway. (HostHatch Docs)

On db1:

sudo nano /etc/systemd/network/10-private.network

Put:

[Match]
Name=enp2s0

[Network]
Address=192.168.10.1/24

Then:

sudo networkctl reload
sudo networkctl reconfigure enp2s0

Check:

ip addr show enp2s0
networkctl status enp2s0

You should see:

192.168.10.1/24

Repeat on db2 and db3, changing the address.


Step 3 — Test the private network

From db1:

ping 192.168.10.2

and:

ping 192.168.10.3

From db2:

ping 192.168.10.1

and:

ping 192.168.10.3

And from db3:

ping 192.168.10.1

and:

ping 192.168.10.2

Don’t proceed until all six tests work.

The private network is particularly nice here because HostHatch says traffic on it is unlimited and isn’t counted against the VM’s normal bandwidth allowance. (HostHatch Docs)


Step 4 — Configure hostnames

On db1:

sudo hostnamectl set-hostname db1.example.com

On db2:

sudo hostnamectl set-hostname db2.example.com

On db3:

sudo hostnamectl set-hostname db3.example.com

Then make sure every node can resolve all three names.

For a small private cluster, /etc/hosts is perfectly reasonable.

On all three servers:

sudo nano /etc/hosts

Add:

192.168.10.1    db1.example.com db1
192.168.10.2    db2.example.com db2
192.168.10.3    db3.example.com db3

Then:

ping db1
ping db2
ping db3

Step 5 — Update Debian

Do this on all three:

sudo apt update
sudo apt full-upgrade -y
sudo reboot

Reconnect after the reboot.


Step 6 — Install MariaDB/Galera

Here’s where I want to be careful about versions.

Since you’re using Debian 13, I recommend using a current MariaDB release that explicitly supports Debian 13 rather than blindly following an old Galera tutorial. MariaDB currently publishes Debian 13 packages including Galera 4; current MariaDB releases include Debian 13 builds. (MariaDB)

We should choose the exact MariaDB version before installing it, because all three Galera nodes need to be compatible.

I would currently lean toward a supported MariaDB 11.x release rather than an old tutorial’s MariaDB version.


Step 7 — Firewall

Before we start Galera, we need to allow the Galera traffic only over the private network.

Galera uses several ports/protocols, including:

3306   MariaDB
4567   Galera replication
4568   Galera IST
4444   SST

We’ll restrict those to:

192.168.10.0/24

rather than exposing them to the Internet.

This is important.

Your public interface should not be accepting Galera replication traffic from arbitrary Internet hosts.


Step 8 — Galera configuration

The important settings will eventually look approximately like:

[mysqld]
bind-address = 0.0.0.0

binlog_format = ROW
default_storage_engine = InnoDB
innodb_autoinc_lock_mode = 2

wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so

wsrep_cluster_name = my-galera-cluster

wsrep_cluster_address = gcomm://db1,db2,db3

wsrep_node_name = db1
wsrep_node_address = 192.168.10.1

On db2, the node-specific values become:

wsrep_node_name = db2
wsrep_node_address = 192.168.10.2

And db3:

wsrep_node_name = db3
wsrep_node_address = 192.168.10.3

The exact provider path and package configuration depend on the MariaDB version we install, so don’t paste this configuration yet. We’ll use the configuration appropriate to the installed package.

MariaDB’s current Galera documentation uses the same fundamental architecture: wsrep_on, a Galera provider, a gcomm:// cluster address, and ROW binlogging. (MariaDB)


Step 9 — Bootstrap db1

This is the one step where we have to be particularly careful.

The first node is bootstrapped to create the initial Galera primary component.

MariaDB provides:

galera_new_cluster

for this purpose. MariaDB’s own HA documentation demonstrates bootstrapping the first node this way, then starting the remaining nodes normally. (MariaDB)

On db1 only:

sudo galera_new_cluster

Then:

sudo mariadb

Check:

SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';

Initially:

wsrep_cluster_size
1

And:

SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';

should show:

Synced

Don’t proceed until db1 is healthy.


Step 10 — Join db2

Once db1 is healthy, go to db2:

sudo systemctl start mariadb

Then check:

sudo mariadb

and:

SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';

You should now see:

2

Check db1 again:

SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';

It should also show:

2

Step 11 — Join db3

On db3:

sudo systemctl start mariadb

Then check:

SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';

You should get:

3

And:

SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';

should show:

Synced

At that point:

        ┌───────────┐
        │   db1     │
        │  Synced   │
        └─────┬─────┘
              │
       ┌──────┴──────┐
       │             │
 ┌─────▼─────┐ ┌─────▼─────┐
 │    db2    │ │    db3    │
 │  Synced   │ │  Synced   │
 └───────────┘ └───────────┘

     wsrep_cluster_size = 3

You now have your three-node Galera cluster.


But don’t stop there

Once the basic cluster works, there are several things I’d configure before putting WordPress/Nextcloud/LAMP production databases on it:

  1. Firewall
  2. MariaDB security
  3. Galera SST authentication
  4. Proper SST method
  5. Database users
  6. ProxySQL
  7. Monitoring
  8. B2 backups
  9. Galera recovery procedures
  10. Testing node failure
  11. Testing complete cluster failure
  12. Testing restoration from backup

And particularly important for your 4-GB nodes: we’ll tune MariaDB’s memory usage conservatively rather than assuming a large server.

One thing I recommend changing from our earlier plan

Because HostHatch provides private networking in the same location, I’d use that for Galera replication and reserve WireGuard for your broader infrastructure network. HostHatch’s private network is isolated at the VLAN/VXLAN level and doesn’t count against your VM’s normal bandwidth allowance. (HostHatch Docs)

So your eventual network becomes:

             INTERNET
                 │
          Public interface
                 │
          ┌──────┴──────┐
          │             │
        Web 1         Web 2
          │             │
          └──────┬──────┘
                 │
              ProxySQL
                 │
       HostHatch PRIVATE NETWORK
                 │
        ┌────────┼────────┐
        │        │        │
       db1      db2      db3
        │        │        │
        └────────┼────────┘
              GALERA

Let’s do this interactively rather than giving you 50 commands at once. If you’ve already created the three VPSs, start with db1 and run:

hostnamectl
ip -br addr
ip -br link
ip -o link | grep 00:22

Paste the output here. I’ll tell you exactly what to configure for the HostHatch private interface before we touch MariaDB.

How to Isolate Sending Permissions with Amazon SES

To isolate sending permissions so that Website A cannot send emails using Website B’s domain, you need to create dedicated IAM Users for each website with a restricted IAM Policy.
The easiest and cleanest way to do this is using the AWS Console.

Step 1: Get the ARN of Your Domain Identity in SES

  1. Go to Configuration $\rightarrow$ Verified identities.
  2. Click on your website’s domain (e.g., example.com).
  3. Copy the Identity ARN at the top of the details page. It looks like this:

    Plaintext

    arn:aws:ses:us-east-1:123456789012:identity/example.com
    

Step 2: Create a Restricted IAM Policy for the Website

  1. Open the IAM Console.
  2. In the left menu, click Policies $\rightarrow$ Create policy.
  3. Select the JSON tab and paste the following policy (replace the Resource ARN with the one you copied in Step 1):

JSON

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowListAndVerifySES",
            "Effect": "Allow",
            "Action": [
                "ses:ListIdentities",
                "ses:GetIdentityVerificationAttributes"
            ],
            "Resource": "*"
        },
        {
            "Sid": "RestrictSESToSpecificDomain",
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "arn:aws:ses:us-east-1:123456789012:identity/example.com"
        }
    ]
}
What this does: It permits the user to send emails only if the “From” address belongs to example.com (or subdomains like mail.example.com). If Website A attempts to spoof Website B’s email address, SES will reject it.
  1. Click Next, name the policy SES-Send-Policy-example.com, and click Create policy.

Step 3: Create the IAM User & Get Credentials

Depending on how your website sends emails (via SMTP or AWS SDK/API), follow the corresponding method below:

Option A: If your website connects via SMTP (WordPress, Joomla, etc.)

  1. Go to the IAM Console $\rightarrow$ Users $\rightarrow$ Create user.
  2. Name the user (e.g., smtp-user-example.com).
  3. Under Permissions options, select Attach policies directly.
  4. Search for and select the custom policy you created (SES-Send-Policy-example.com).
  5. Click Next $\rightarrow$ Create user.
  6. Click on the newly created user $\rightarrow$ go to the Security credentials tab.
  7. Scroll down to Access keys $\rightarrow$ click Create access key.
  8. Select Application running outside AWS $\rightarrow$ click Next $\rightarrow$ Create access key.
  9. Copy your Access Key ID and Secret Access Key.
  10. Convert to SMTP Credentials:
    AWS Access Keys are not raw SMTP passwords. To convert your Secret Access Key into an SES SMTP password:
    • In the SES Console, go to SMTP settings.
    • (Or) Use an online convert tool / Python script (AWS Official Converter) to generate the SMTP Password from your Access Key.

Option B: If your app uses the AWS SDK/API (Laravel, Node.js, Python, etc.)

  1. Follow steps 1–8 from Option A.
  2. Put the Access Key ID and Secret Access Key directly into your application’s .env file or environment settings (e.g., AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).

Repeat for Each Additional Website

For every new website:
  1. Copy its domain Identity ARN from SES.
  2. Create a new IAM Policy containing that specific ARN.
  3. Create a dedicated IAM User attached only to that policy.

Bonus Tip: How to enforce strict “From” address matching

If you want to prevent a user from sending as another email on the same domain (e.g., force website1 to only send as noreply@example.com and not admin@example.com), add a condition to your IAM Policy:

JSON

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "arn:aws:ses:us-east-1:123456789012:identity/example.com",
            "Condition": {
                "StringEquals": {
                    "ses:FromAddress": "noreply@example.com"
                }
            }
        }
    ]
}

How to Install AI Tools Locally with Docker

Fooocus

# docker pull ghcr.io/lllyasviel/fooocus:latest
# docker run -d \
–name fooocus \
–gpus all \
-p 7865:7865 \
-v fooocus_data:/content/data \
ghcr.io/lllyasviel/fooocus:latest

VideoSOS

Clone the repo

# git clone https://github.com/timoncool/videosos
# cd videosos

Start VideoSOS in Docker

# docker compose up -d

Open in browser

http://localhost:3000

Stop when done

# docker compose down

LocalAI + Open WebUI

Prereqs

Make sure these work first:

# docker –version
# docker compose version

If you have a GPU (NVIDIA):

NVIDIA drivers installed

NVIDIA Container Toolkit installed

Create a project folder

# mkdir localai-webui
# cd localai-webui

docker-compose.yml

Create this file:

version: “3.9”

services:
localai:
image: ghcr.io/go-skynet/localai:latest
container_name: localai
ports:
– “8080:8080”
volumes:
– ./models:/models
environment:
– MODELS_PATH=/models
command: >
–models-path /models
–context-size 4096
deploy:
resources:
reservations:
devices:
– capabilities: [gpu]
restart: unless-stopped

webui:
image: ghcr.io/open-webui/open-webui:latest
container_name: open-webui
ports:
– “3000:8080”
environment:
– OPENAI_API_BASE_URL=http://localai:8080/v1
– OPENAI_API_KEY=localai
depends_on:
– localai
volumes:
– ./webui-data:/app/backend/data
restart: unless-stopped

Download a model (important)

LocalAI does not auto-download models.

Create folders:

# mkdir -p models/llama-3

Example: download a GGUF model (recommended):

# wget -O models/llama-3/llama-3-8b-instruct.Q4_K_M.gguf \
https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/llama-3-8b-instruct.Q4_K_M.gguf

Create models/llama-3/model.yaml:

name: llama-3
backend: llama-cpp
parameters:
model: llama-3-8b-instruct.Q4_K_M.gguf
context_size: 4096

Start everything
# docker compose up -d

Open the UI

Web UI: http://localhost:3000

LocalAI API: http://localhost:8080/v1/chat/completions

In Open WebUI:

Model → select llama-3

Start chatting

MusicGPT

# docker pull gabotechs/musicgpt
# docker run -it –gpus all -p 8642:8642 \
-v ~/.musicgpt:/root/.local/share/musicgpt \
gabotechs/musicgpt –gpu –ui-expose

ARM Support

If you need ARM support for any of the above, the general approach is:

Build your own ARM image

Enable Docker Buildx

# docker buildx create –use
# docker buildx inspect –bootstrap

Clone the repo

# git clone https://github.com/<project>/<repo>.git
# cd <repo>

Build for ARM64

# docker buildx build \
–platform linux/arm64 \
-t my-arm64-fooocus:latest \
.

Run it

# docker run -p 7865:7865 my-arm64-fooocus:latest

This works if the Python/pytorch packages and other dependencies have ARM-compatible wheels — many PyTorch builds do now on macOS M1/M2 and some Linux ARM64 systems.

Useful MinIO scripts

nextcloud_archive_files.sh

#!/bin/bash
set -o pipefail

#############################################
# CONFIGURATION
#############################################
NEXTCLOUD_DATA_DIR="/path/to/nextcloud/data"
MINIO_ALIAS="myminio"
MINIO_BUCKET="bucket_name"
LOG_FILE="/path/to/minio.log"

# DRY RUN (set to false to actually upload & delete)
DRY_RUN=false

# Disable MC color output to avoid ANSI codes
export MC_COLOR=off

mkdir -p "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"

#############################################
# SANITIZATION FUNCTIONS
#############################################

sanitize_string() {
    printf "%s" "$1" | tr -d '\000-\037\177'
}

log() {
    local clean_msg
    clean_msg=$(sanitize_string "$1")
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $clean_msg" >> "$LOG_FILE"
}

sanitize_file_on_disk() {
    local file="$1"
    local dir base cleanbase

    dir=$(dirname "$file")
    base=$(basename "$file")
    cleanbase=$(printf "%s" "$base" | tr -d '\000-\037\177')

    if [[ "$cleanbase" != "$base" ]]; then
        mv -n -- "$file" "$dir/$cleanbase"
        log "Renamed on disk: '$file' → '$dir/$cleanbase'"
    fi

    echo "$dir/$cleanbase"
}

#############################################
# ARCHIVE PROCESS
#############################################

log "===== Starting Nextcloud archive process ====="
log "DRY_RUN=$DRY_RUN"



find "$NEXTCLOUD_DATA_DIR" \
    -xdev \
    -type f \
    -mtime +90 \
    -size +200M \
    -print0 |
while IFS= read -r -d '' file; do

    # Sanitize filename on disk
    file=$(sanitize_file_on_disk "$file")

    # Build relative path
    relative_path="${file#$NEXTCLOUD_DATA_DIR/}"
    relative_path=$(sanitize_string "$relative_path")

    target="$MINIO_ALIAS/$MINIO_BUCKET/$relative_path"

    log "Processing: '$file' → '$target'"

    if [[ "$DRY_RUN" == true ]]; then
        log "DRY-RUN: Would upload and delete '$file'"
        continue
    fi

    mc_output=$(mc cp -- "$file" "$target" 2>&1 | tr -d '\000-\037\177')
    mc_exit=$?

    if [[ $mc_exit -eq 0 ]]; then
        log "SUCCESS: Uploaded '$file'"
        rm -f -- "$file"
        log "Deleted local copy: '$file'"
    else
        log "ERROR: Upload failed for '$file'"
        log "mc error: $mc_output"
    fi

done

log "===== Completed Nextcloud archive process ====="

create_user.sh

#!/bin/bash

# MinIO Server Details
MINIO_HOST="http://MINIO_HOST:9000"  # replace with your MinIO endpoint
MINIO_ROOT_USER="########"          # replace with your root username
MINIO_ROOT_PASSWORD="#########"      # replace with your root password

# MinIO Client (mc) alias setup
MC_ALIAS="myminio"

# Check if bucket name is provided as an argument
if [ -z "$1" ]; then
    echo "Usage: $0 "
    exit 1
fi

# Bucket name passed as argument
BUCKET="$1"
USERNAME="${BUCKET}-user"
POLICY_NAME="policy-${BUCKET}"

# Set mc alias
mc alias set $MC_ALIAS $MINIO_HOST $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD

# Create the user
create_user() {
    # Create user with a randomly generated secret key (or use your own password)
    USER_SECRET=$(openssl rand -base64 32)
    mc admin user add $MC_ALIAS $USERNAME $USER_SECRET

    # Create a custom policy for the bucket
    cat < /tmp/${POLICY_NAME}.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::${BUCKET}"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::${BUCKET}/*"
    }
  ]
}
EOF

    # Attach the policy to the user
    mc admin policy add $MC_ALIAS $POLICY_NAME /tmp/${POLICY_NAME}.json
    mc admin policy attach $MC_ALIAS $POLICY_NAME --user $USERNAME

    # Clean up the policy file
    rm /tmp/${POLICY_NAME}.json

    echo "User $USERNAME created with policy for bucket $BUCKET"
    echo "User credentials: ACCESSKEY: $USERNAME, SECRETKEY: $USER_SECRET"
}

# Check if the bucket exists before creating the user and policy
mc ls $MC_ALIAS/$BUCKET &> /dev/null
if [ $? -ne 0 ]; then
    echo "Bucket $BUCKET does not exist. Please create the bucket first."
    exit 1
fi

# Call the function to create the user and attach the policy
create_user

A Script for Backing Up Nextcloud to an FTP Server

#!/bin/bash

# ==== CONFIGURATION ====
NEXTCLOUD_PATH="/var/www/html"
DATA_PATH="/var/www/html/data"
BACKUP_PATH="/var/backups/nextcloud"

DB_USER="nextcloud"
DB_PASS="dbpassword"
DB_NAME="nextcloud"

FTP_HOST="yourftphost.com"
FTP_USER="yourftpuser"
FTP_PASS="yourftppassword"
FTP_DIR="/path/to/save/file"

DATE=$(date +"%Y-%m-%d_%H-%M")
ARCHIVE_NAME="nextcloud-backup-$DATE.tar.gz"

MAX_LOCAL_BACKUPS=3

# ==== START BACKUP ====
echo "Starting streamed Nextcloud backup..."

mkdir -p "$BACKUP_PATH"

# Export database (small file)
DB_DUMP="/tmp/db_$DATE.sql"
mysqldump --no-tablespaces -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$DB_DUMP"

# === Stream tar directly to FTP ===
echo "Creating and streaming archive directly to FTP..."

tar -czf - \
--exclude="$DATA_PATH/appdata_*/preview" \
--exclude="$NEXTCLOUD_PATH/updater-*" \
--exclude="$NEXTCLOUD_PATH/data/tmp" \
-C / \
"${NEXTCLOUD_PATH#/}" \
"${DATA_PATH#/}" \
"$DB_DUMP" \
| curl --ftp-pasv -T - "ftp://$FTP_HOST$FTP_DIR/$ARCHIVE_NAME" --user "$FTP_USER:$FTP_PASS"

# Remove temp database dump
rm -f "$DB_DUMP"

# Optional: keep a few local backups (not needed for streamed uploads)
echo "Cleaning up old local backups..."
cd "$BACKUP_PATH" || exit
ls -1t nextcloud-backup-*.tar.gz 2>/dev/null | tail -n +$((MAX_LOCAL_BACKUPS + 1)) | xargs -r rm -f --

echo "Backup complete: streamed directly to FTP as $ARCHIVE_NAME"

Setting up a NAS on a Raspberry Pi 5 that boots from NVME

# curl https://download.argon40.com/argonneo5.sh | bash
# nano /boot/firmware/config.txt

kernel=kernel8.img
dtparam=nvme
dtparam=pciex1_gen=3
usb_max_current_enable=1

# sudo apt update
# sudo apt install snapd
# sudo snap install snapd
# sudo snap install nextcloud
# sudo /snap/bin/nextcloud.occ config:system:set trusted_proxies 0 --value="127.0.0.1"
# sudo /snap/bin/nextcloud.occ config:system:set trusted_domains 1 --value="yourdomain.com"
# sudo /snap/bin/nextcloud.disable-https
# sudo apt install caddy
# sudo nano /etc/caddy/Caddyfile

https://yourdomain.com {
    reverse_proxy 127.0.0.1:8080
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
    }

    # Redirect for CalDAV and CardDAV clients
    redir /.well-known/caldav /remote.php/dav 301
    redir /.well-known/carddav /remote.php/dav 301
}

# sudo snap set nextcloud ports.http=8080
# sudo snap set nextcloud ports.https=
# sudo snap restart nextcloud.apache
# sudo systemctl daemon-reload
# sudo systemctl start caddy
# sudo systemctl status caddy
# sudo /snap/bin/nextcloud.occ config:system:set overwriteprotocol --value="https"
# sudo /snap/bin/nextcloud.occ config:system:set overwrite.cli.url --value="https://yourdomain.com"
# sudo /snap/bin/nextcloud.occ maintenance:repair --include-expensive
# sudo /snap/bin/nextcloud.occ background:cron
# apt install syncthing
# systemctl enable syncthing@pi-nas.service
# systemctl start syncthing@pi-nas.service
# nano /usr/lib/systemd/system/syncthing@.service

Group=users
UMask=0002

# apt install proftpd
# nano /etc/proftpd/proftpd.conf

DefaultRoot ~
Umask 002 002

# nano /usr/local/bin/fix_ownership.sh

#!/bin/bash
WATCHDIR="/volume1"
LOGFILE="/var/log/fix_ownership.log"

# Function to fix ownership of a file or directory
fix_ownership() {
    local file="$1"
    if [ -e "$file" ]; then
        if [ -d "$file" ]; then
            chown -R pi-nas:users "$file" 2>/dev/null
        else
            chown pi-nas:users "$file" 2>/dev/null
        fi
        echo "$(date '+%Y-%m-%d %H:%M:%S') Fixed ownership: $file" >> "$LOGFILE"
    fi
}

# Watch for changes
inotifywait -m -r -e close_write,create,move,delete --format '%w%f' "$WATCHDIR" | while read -r file; do
    fix_ownership "$file"
done

# nano /etc/systemd/system/fix_ownership.service

[Unit]
Description=Recursive ownership watcher for Volume1 folder
After=network.target

[Service]
ExecStart=/usr/local/bin/fix_ownership.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

# systemctl daemon-reload
# systemctl enable fix_ownership.service
# systemctl start fix_ownership.service

Setting Up Radicale and Infcloud in Debian 13 (Trixie)

# apt update
# apt install git python3-pip python3-venv
# useradd -r -m -d /var/lib/radicale -s /bin/bash radicale
# mkdir /etc/radicale
# nano /etc/radicale/radicale.conf

[server]
# Bind to all IP addresses (0.0.0.0) instead of just localhost
hosts = 0.0.0.0:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = bcrypt
[storage]
filesystem_folder = /var/lib/radicale/collections
[web]
type = radicale_infcloud

# htpasswd -c -B /etc/radicale/users yourusername
# htpasswd -B /etc/radicale/users anotheruser
# ufw allow 5232/tcp
# ufw reload
# su radicale
$ cd ~
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install radicale bcrypt git+https://github.com/Unrud/RadicaleInfCloud.git
$ radicale --config /etc/radicale/radicale.conf

http://your-server-ip:5232/

$ exit
# nano /etc/systemd/system/radicale.service

[Unit]
Description=Radicale CalDAV and CardDAV Server
After=network.target
[Service]
User=radicale
Group=radicale
ExecStart=/var/lib/radicale/venv/bin/radicale --config /etc/radicale/radicale.conf
WorkingDirectory=/var/lib/radicale
Restart=on-failure
LimitNOFILE=4096
[Install]
WantedBy=multi-user.target

# systemctl daemon-reload
# systemctl enable radicale
# systemctl start radicale
# systemctl status radicale

# apt install apache2 certbot python3-certbot-apache
# nano /etc/apache2/sites-available/radicale.conf

<VirtualHost *:80>
  ServerName your_domain.com
  ProxyPass / http://localhost:5232/
  ProxyPassReverse / http://localhost:5232/
</VirtualHost>

# ufw allow 80/tcp
# ufw allow 443/tcp
# ufw reload
# certbot --apache -d your_domain.com

https://your_domain.com

# sudo systemctl status certbot.timer
# sudo certbot renew --dry-run

Setting Up Webmin/Fail2ban/Freeswitch in Debian

Webmin Setup

The simplest and best way to get Webmin is to use automatic webmin-setup-repo.sh script to configure repositories on your RHEL or Debian derivative systems. It can be done in two easy steps:

$curl -o webmin-setup-repo.sh https://raw.githubusercontent.com/webmin/webmin/master/webmin-setup-repo.sh
sudo sh webmin-setup-repo.sh

This script will automatically setup our repository and install our GPG keys on your system, and provide webmin package for installation and easy upgrades in the future.

Install

If Webmin repository was setup using our webmin-setup-repo.sh as described above then Webmin can be installed as easy as:

$ sudo apt-get install webmin --install-recommends

Access

Open ports for both Webmin and Freeswitch.

$ ufw allow 10000/tcp
$ ufw allow 3478:3479/udp
$ ufw allow 5060
$ ufw allow 5080
$ ufw allow 8021/tcp
$ ufw allow 16384:32768/udp

After successful Webmin installation, you can access its interface by entering https://<Your-Server-IP>:10000 in your browser..

Freeswitch Setup

$curl -sSL https://freeswitch.org/fsget | bash -s [Personal Access Token] release install
$ sudo apt install freeswitch-mod-flite fail2ban

Fail2ban Setup

Fail2ban’s jail.conf file contains a standard configuration for FreeSWITCH.

From Standard jail.conf

[shd] <-- Make sure that this section is empty.

[freeswitch]
enabled  = true
port     = 5060,5061
action_  = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"]
           %(default/action_)s[name=%(__name__)s-udp, protocol="udp"]
logpath  = /var/log/freeswitch/freeswitch.log
filter   = freeswitch-ip

/etc/fail2ban/filter.d/freeswitch-ip.conf

# Fail2Ban configuration file
[Definition]
failregex = \[WARNING\] sofia_reg.c:\d+ Can't find user \[.*@\d+.\d+.\d+.\d+\] from <HOST>
ignoreregex =

Add the following line to the [DEFAULT] section of /etc/fail2ban/paths-debian.conf:

sshd_backend = systemd

In Webmin, go to Networking > Fail2ban Intrusion Detector and click on Filter Action Jails. Click on Freeswitch in the list and change ‘Check for log file updates using‘ to systemd and click on the ‘Save’ button.

Usage

To check the status of fail2ban:

$ systemctl status fail2ban.service

To check the IP addresses that were blocked:

$ fail2ban-client banned

Resources

https://webmin.com/download/

https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Installation/Linux/Debian_67240088#about

https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Security/Fail2Ban_1049236/

https://github.com/fail2ban/fail2ban/issues/3567

https://stackoverflow.com/questions/3561289/what-ports-does-freeswitch-need-open

Setting Up a Syncthing System Service

Create the user who should run the service, or choose an existing one.(Skip if your distribution package already installs these files, see above.) From git location copy the syncthing@.service file into the load path of the system instance. On Debian 12, the path of the file is ‘/usr/lib/systemd/system’. I also added the following lines to the [Service] section of the file:

Group=users
UMask=0002

The umask only works when ‘Ignore Permissions’ is enabled for the folder. Enable and start the service. Replace “myuser” with the actual Syncthing user after the @:

$ sudo systemctl enable syncthing@myuser.service
$ sudo systemctl start syncthing@myuser.service

Reference

https://docs.syncthing.net/users/autostart.html

Setting Up IMAPdump in Debian

IMAPdump, requires OpenSSL to be installed. Install it using the following command:

$ sudo apt install openssl

The IO::Socket::SSL Perl module is also required. First, install using the following command:

$ sudo perl -MCPAN -e shell

Then enter the following command from within the cpan shell:

cpan[1]> install IO::Socket::SSL

Now IMAPdump should run without errors. You can download it from https://github.com/andrewnimmo/rick-sanders-imap-tools.