feat(apps): Convert Docker-based apps to LXC

Converted secubox-app-jellyfin, secubox-app-mailserver, and added
secubox-app-roundcube to use LXC containers instead of Docker.

Changes:
- jellyfinctl: Now uses LXC at 192.168.255.31
- mailserverctl: New controller for Alpine LXC with Postfix/Dovecot
- roundcubectl: New package for Roundcube webmail LXC

All controllers support:
- Bootstrap Alpine rootfs using static apk
- LXC configuration generation
- HAProxy integration with waf_bypass
- Start/stop/status commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-14 09:07:33 +01:00
parent 3d78b22d85
commit 2b8fb1cd62
6 changed files with 1255 additions and 466 deletions

View File

@ -1,27 +1,26 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-jellyfin PKG_NAME:=secubox-app-jellyfin
PKG_VERSION:=1.0.0 PKG_VERSION:=3.0.0
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr> PKG_MAINTAINER:=SecuBox <info@secubox.in>
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=AGPL-3.0
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-jellyfin define Package/secubox-app-jellyfin
SECTION:=utils SECTION:=secubox
CATEGORY:=Utilities CATEGORY:=SecuBox
SUBMENU:=Apps
PKGARCH:=all PKGARCH:=all
SUBMENU:=SecuBox Apps TITLE:=SecuBox Jellyfin Media Server (LXC)
TITLE:=SecuBox Jellyfin media server DEPENDS:=+lxc +curl
DEPENDS:=+dockerd +docker +containerd
endef endef
define Package/secubox-app-jellyfin/description define Package/secubox-app-jellyfin/description
Installer, configuration, and service manager for running Jellyfin Jellyfin media server running in LXC container.
inside Docker on SecuBox-powered OpenWrt systems. Free media server Free media server for streaming movies, TV shows, music, and photos.
for streaming movies, TV shows, music, and photos.
endef endef
define Package/secubox-app-jellyfin/conffiles define Package/secubox-app-jellyfin/conffiles
@ -47,16 +46,15 @@ define Package/secubox-app-jellyfin/postinst
[ -n "$${IPKG_INSTROOT}" ] || { [ -n "$${IPKG_INSTROOT}" ] || {
echo "" echo ""
echo "============================================" echo "============================================"
echo " Jellyfin Media Server Installed" echo " Jellyfin Media Server (LXC) Installed"
echo "============================================" echo "============================================"
echo "" echo ""
echo "Quick Start:" echo "Quick Start:"
echo " 1. Install: jellyfinctl install" echo " 1. Extract rootfs from Docker image (see README)"
echo " 2. Configure media: uci add_list jellyfin.media.media_path='/path/to/media'" echo " 2. Install: jellyfinctl install"
echo " 3. Commit: uci commit jellyfin" echo " 3. Start: jellyfinctl start"
echo " 4. Start: /etc/init.d/jellyfin start"
echo "" echo ""
echo "Web UI: http://<device-ip>:8096" echo "Web UI: http://192.168.255.31:8096"
echo "" echo ""
} }
exit 0 exit 0

View File

@ -1,11 +1,11 @@
#!/bin/sh #!/bin/sh
# SecuBox Jellyfin Media Server manager # SecuBox Jellyfin Media Server manager
# Full integration: Docker, HAProxy, Firewall, Mesh P2P, Backup/Restore # LXC-based deployment with HAProxy, Firewall, Mesh P2P integration
VERSION="2.0.0" VERSION="3.0.0"
CONFIG="jellyfin" CONFIG="jellyfin"
CONTAINER="secbx-jellyfin" CONTAINER="jellyfin"
OPKG_UPDATED=0 LXC_PATH="/srv/lxc/jellyfin"
# Colors # Colors
RED='\033[0;31m' RED='\033[0;31m'
@ -29,83 +29,68 @@ require_root() {
} }
defaults() { defaults() {
image="$(uci_get main.image)"
[ -z "$image" ] && image="jellyfin/jellyfin:latest"
data_path="$(uci_get main.data_path)" data_path="$(uci_get main.data_path)"
[ -z "$data_path" ] && data_path="/srv/jellyfin" [ -z "$data_path" ] && data_path="/srv/jellyfin"
media_path="$(uci_get main.media_path)"
[ -z "$media_path" ] && media_path="/srv/SHARE"
port="$(uci_get main.port)" port="$(uci_get main.port)"
[ -z "$port" ] && port="8096" [ -z "$port" ] && port="8096"
timezone="$(uci_get main.timezone)" ip_address="$(uci_get main.ip_address)"
[ -z "$timezone" ] && timezone="Europe/Paris" [ -z "$ip_address" ] && ip_address="192.168.255.31"
hw_accel="$(uci_get transcoding.hw_accel)"
[ -z "$hw_accel" ] && hw_accel="0"
gpu_device="$(uci_get transcoding.gpu_device)"
domain="$(uci_get network.domain)" domain="$(uci_get network.domain)"
[ -z "$domain" ] && domain="jellyfin.secubox.local" [ -z "$domain" ] && domain="jellyfin.secubox.local"
} }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
ensure_packages() { # ============================================================================
for pkg in "$@"; do # LXC Helpers
if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then # ============================================================================
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1 lxc_running() {
OPKG_UPDATED=1 lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING"
fi
opkg install "$pkg" || return 1
fi
done
} }
# ============================================================================ lxc_exists() {
# Prerequisite Checks [ -d "$LXC_PATH/rootfs" ]
# ============================================================================ }
check_prereqs() { create_lxc_config() {
defaults defaults
ensure_dir "$data_path" cat > "$LXC_PATH/config" << EOF
[ -d /sys/fs/cgroup ] || { error "/sys/fs/cgroup missing"; return 1; } lxc.uts.name = jellyfin
ensure_packages dockerd docker containerd lxc.rootfs.path = dir:${LXC_PATH}/rootfs
/etc/init.d/dockerd enable >/dev/null 2>&1 lxc.net.0.type = veth
/etc/init.d/dockerd start >/dev/null 2>&1 lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.ipv4.address = ${ip_address}/24
lxc.net.0.ipv4.gateway = 192.168.255.1
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = ${media_path} srv/SHARE none bind,ro 0 0
lxc.mount.entry = ${data_path}/config config none bind 0 0
lxc.mount.entry = ${data_path}/cache cache none bind 0 0
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.seccomp.profile =
lxc.tty.max = 0
lxc.pty.max = 256
lxc.cgroup2.memory.max = 2048000000
lxc.init.cmd = /opt/start-jellyfin.sh
EOF
} }
# ============================================================================ create_startup_script() {
# Docker Helpers cat > "$LXC_PATH/rootfs/opt/start-jellyfin.sh" << 'EOF'
# ============================================================================ #!/bin/bash
export PATH=/usr/lib/jellyfin-ffmpeg:$PATH
export LD_LIBRARY_PATH=/usr/lib/jellyfin-ffmpeg/lib:$LD_LIBRARY_PATH
pull_image() { echo "Starting Jellyfin..."
defaults exec /jellyfin/jellyfin \
docker pull "$image" --datadir=/config \
} --cachedir=/cache \
--webdir=/jellyfin/jellyfin-web
stop_container() { EOF
docker stop "$CONTAINER" >/dev/null 2>&1 || true chmod +x "$LXC_PATH/rootfs/opt/start-jellyfin.sh"
docker rm "$CONTAINER" >/dev/null 2>&1 || true
}
build_media_mounts() {
local mounts=""
local paths
paths=$(uci -q get ${CONFIG}.media.media_path)
if [ -n "$paths" ]; then
for p in $paths; do
[ -d "$p" ] && mounts="$mounts -v ${p}:${p}:ro"
done
fi
echo "$mounts"
}
build_gpu_args() {
if [ "$hw_accel" = "1" ]; then
local dev="${gpu_device:-/dev/dri}"
if [ -e "$dev" ]; then
echo "--device=${dev}:${dev}"
else
echo ""
fi
fi
} }
# ============================================================================ # ============================================================================
@ -123,155 +108,39 @@ configure_haproxy() {
defaults defaults
local ssl=$(uci_get network.haproxy_ssl) # Check if backend already exists
local ssl_redirect=$(uci_get network.haproxy_ssl_redirect) if uci -q get haproxy.jellyfin_web >/dev/null 2>&1; then
log "HAProxy backend jellyfin_web already exists, updating..."
# Check if vhost already exists (idempotent) uci set haproxy.jellyfin_web.server="media ${ip_address}:${port} weight 100 check"
local existing=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1)
if [ -n "$existing" ]; then
log "HAProxy vhost for $domain already configured"
return 0
fi
log "Configuring HAProxy for $domain..."
# Add backend
uci -q add haproxy backend
uci -q set haproxy.@backend[-1].name='jellyfin_web'
uci -q set haproxy.@backend[-1].mode='http'
uci -q add_list haproxy.@backend[-1].server="jellyfin 192.168.255.1:$port check"
# Add vhost
uci -q add haproxy vhost
uci -q set haproxy.@vhost[-1].enabled='1'
uci -q set haproxy.@vhost[-1].domain="$domain"
uci -q set haproxy.@vhost[-1].backend='jellyfin_web'
uci -q set haproxy.@vhost[-1].ssl="${ssl:-1}"
uci -q set haproxy.@vhost[-1].ssl_redirect="${ssl_redirect:-1}"
uci -q set haproxy.@vhost[-1].websocket='1'
uci commit haproxy
/etc/init.d/haproxy reload 2>/dev/null
log "HAProxy configured for $domain"
}
remove_haproxy() {
if ! command -v haproxyctl >/dev/null 2>&1; then
return 0
fi
defaults
log "Removing HAProxy configuration for $domain..."
# Find and remove backend
local idx=0
while uci -q get haproxy.@backend[$idx] >/dev/null 2>&1; do
local name=$(uci -q get haproxy.@backend[$idx].name)
if [ "$name" = "jellyfin_web" ]; then
uci delete haproxy.@backend[$idx]
break
fi
idx=$((idx + 1))
done
# Find and remove vhost
idx=0
while uci -q get haproxy.@vhost[$idx] >/dev/null 2>&1; do
local vdomain=$(uci -q get haproxy.@vhost[$idx].domain)
if [ "$vdomain" = "$domain" ]; then
uci delete haproxy.@vhost[$idx]
break
fi
idx=$((idx + 1))
done
uci commit haproxy
/etc/init.d/haproxy reload 2>/dev/null
log "HAProxy configuration removed"
}
# ============================================================================
# Firewall
# ============================================================================
configure_firewall() {
local fw_wan=$(uci_get network.firewall_wan)
[ "$fw_wan" != "1" ] && { log "WAN firewall rule disabled in UCI"; return 0; }
defaults
# Idempotent: check if rule already exists
if uci show firewall 2>/dev/null | grep -q "Jellyfin-HTTP"; then
log "Firewall rule for Jellyfin already exists"
return 0
fi
log "Configuring firewall for port $port..."
uci add firewall rule
uci set firewall.@rule[-1].name='Jellyfin-HTTP'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port="$port"
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].target='ACCEPT'
uci set firewall.@rule[-1].enabled='1'
uci commit firewall
/etc/init.d/firewall reload 2>/dev/null
log "Firewall configured"
}
remove_firewall() {
log "Removing firewall rules..."
local idx=0
while uci -q get firewall.@rule[$idx] >/dev/null 2>&1; do
local name=$(uci -q get firewall.@rule[$idx].name)
if [ "$name" = "Jellyfin-HTTP" ]; then
uci delete firewall.@rule[$idx]
uci commit firewall
/etc/init.d/firewall reload 2>/dev/null
log "Firewall rule removed"
return 0
fi
idx=$((idx + 1))
done
}
# ============================================================================
# Mesh Integration
# ============================================================================
register_mesh_service() {
local mesh_enabled=$(uci_get mesh.enabled)
[ "$mesh_enabled" != "1" ] && return 0
defaults
if [ -x /usr/sbin/secubox-p2p ]; then
/usr/sbin/secubox-p2p register-service jellyfin "$port" 2>/dev/null
log "Registered Jellyfin with mesh network"
else else
warn "secubox-p2p not found, skipping mesh registration" log "Creating HAProxy backend..."
uci set haproxy.jellyfin_web=backend
uci set haproxy.jellyfin_web.name='jellyfin_web'
uci set haproxy.jellyfin_web.mode='http'
uci set haproxy.jellyfin_web.balance='roundrobin'
uci set haproxy.jellyfin_web.enabled='1'
uci set haproxy.jellyfin_web.server="media ${ip_address}:${port} weight 100 check"
fi fi
local dns_enabled=$(uci -q get secubox-p2p.dns.enabled || echo "0") # Check if vhost already exists
if [ "$dns_enabled" = "1" ]; then local vhost_name=$(echo "$domain" | tr '.' '_')
local dns_domain=$(uci -q get secubox-p2p.dns.base_domain || echo "mesh.local") if ! uci -q get haproxy.${vhost_name} >/dev/null 2>&1; then
local hostname=$(echo "$domain" | cut -d'.' -f1) log "Creating HAProxy vhost for $domain..."
log "Mesh DNS: $hostname.$dns_domain" uci set haproxy.${vhost_name}=vhost
uci set haproxy.${vhost_name}.domain="$domain"
uci set haproxy.${vhost_name}.backend='jellyfin_web'
uci set haproxy.${vhost_name}.ssl='1'
uci set haproxy.${vhost_name}.ssl_redirect='1'
uci set haproxy.${vhost_name}.acme='1'
uci set haproxy.${vhost_name}.waf_bypass='1'
uci set haproxy.${vhost_name}.enabled='1'
fi fi
}
unregister_mesh_service() { uci commit haproxy
if [ -x /usr/sbin/secubox-p2p ]; then haproxyctl generate 2>/dev/null
/usr/sbin/secubox-p2p unregister-service jellyfin 2>/dev/null haproxyctl reload 2>/dev/null
log "Unregistered Jellyfin from mesh network"
fi log "HAProxy configured for $domain -> ${ip_address}:${port}"
} }
# ============================================================================ # ============================================================================
@ -280,83 +149,94 @@ unregister_mesh_service() {
cmd_install() { cmd_install() {
require_root require_root
log "Installing Jellyfin Media Server..." log "Installing Jellyfin LXC container..."
check_prereqs || exit 1
defaults defaults
ensure_dir "$LXC_PATH"
ensure_dir "$LXC_PATH/rootfs/srv/SHARE"
ensure_dir "$LXC_PATH/rootfs/config"
ensure_dir "$LXC_PATH/rootfs/cache"
ensure_dir "$LXC_PATH/rootfs/opt"
ensure_dir "$data_path/config" ensure_dir "$data_path/config"
ensure_dir "$data_path/cache" ensure_dir "$data_path/cache"
log "Pulling Docker image..." if [ ! -f "$LXC_PATH/rootfs/jellyfin/jellyfin" ]; then
pull_image || exit 1 log "Jellyfin rootfs not found. Please extract from Docker image:"
echo " docker pull jellyfin/jellyfin:latest"
echo " docker create --name temp-jellyfin jellyfin/jellyfin:latest"
echo " docker export temp-jellyfin > /tmp/jellyfin.tar"
echo " tar -xf /tmp/jellyfin.tar -C $LXC_PATH/rootfs/"
echo " docker rm temp-jellyfin"
return 1
fi
create_lxc_config
create_startup_script
# Symlink ffmpeg if needed
if [ -d "$LXC_PATH/rootfs/usr/lib/jellyfin-ffmpeg" ]; then
ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg "$LXC_PATH/rootfs/usr/bin/ffmpeg" 2>/dev/null
ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe "$LXC_PATH/rootfs/usr/bin/ffprobe" 2>/dev/null
fi
uci_set main.enabled '1' uci_set main.enabled '1'
uci commit ${CONFIG} uci commit ${CONFIG}
/etc/init.d/jellyfin enable
# Integrate with HAProxy if configured
configure_haproxy configure_haproxy
# Configure firewall if WAN access requested log "Jellyfin LXC installed successfully!"
configure_firewall log "Start with: jellyfinctl start"
# Register with mesh if enabled
register_mesh_service
log "Jellyfin installed successfully!"
echo ""
echo "Next steps:"
echo " 1. Add media: uci add_list jellyfin.media.media_path='/path/to/media'"
echo " 2. Set domain: uci set jellyfin.network.domain='media.example.com'"
echo " 3. Commit: uci commit jellyfin"
echo " 4. Start: /etc/init.d/jellyfin start"
echo " Web UI: http://<device-ip>:${port}"
echo ""
} }
cmd_uninstall() { # ============================================================================
# Service Control
# ============================================================================
cmd_start() {
require_root require_root
log "Uninstalling Jellyfin..."
/etc/init.d/jellyfin stop 2>/dev/null if lxc_running; then
/etc/init.d/jellyfin disable 2>/dev/null log "Jellyfin already running"
stop_container return 0
fi
# Remove integrations if ! lxc_exists; then
remove_haproxy error "Jellyfin not installed. Run 'jellyfinctl install' first"
remove_firewall return 1
unregister_mesh_service fi
# Remove image
defaults defaults
docker rmi "$image" 2>/dev/null create_lxc_config
uci_set main.enabled '0' log "Starting Jellyfin LXC..."
uci commit ${CONFIG} lxc-start -n "$CONTAINER" -d
sleep 5
log "Jellyfin uninstalled. Data preserved at $data_path" if lxc_running; then
log "To remove data: rm -rf $data_path" log "Jellyfin started at http://${ip_address}:${port}"
else
error "Failed to start Jellyfin"
return 1
fi
} }
# ============================================================================ cmd_stop() {
# Check & Update
# ============================================================================
cmd_check() {
check_prereqs
echo "Prerequisite check completed."
}
cmd_update() {
require_root require_root
log "Pulling latest image..."
pull_image || exit 1 if ! lxc_running; then
log "Restarting service..." log "Jellyfin is not running"
/etc/init.d/jellyfin restart return 0
# Prune old images fi
docker image prune -f 2>/dev/null
log "Update complete" log "Stopping Jellyfin..."
lxc-stop -n "$CONTAINER"
log "Jellyfin stopped"
}
cmd_restart() {
cmd_stop
sleep 2
cmd_start
} }
# ============================================================================ # ============================================================================
@ -368,86 +248,43 @@ cmd_status() {
echo "" echo ""
echo "========================================" echo "========================================"
echo " Jellyfin Media Server v$VERSION" echo " Jellyfin Media Server v$VERSION (LXC)"
echo "========================================" echo "========================================"
echo "" echo ""
local enabled=$(uci_get main.enabled) local enabled=$(uci_get main.enabled)
echo "Configuration:" echo "Configuration:"
echo " Enabled: $([ "$enabled" = "1" ] && echo -e "${GREEN}Yes${NC}" || echo -e "${RED}No${NC}")" echo " Enabled: $([ "$enabled" = "1" ] && echo -e "${GREEN}Yes${NC}" || echo -e "${RED}No${NC}")"
echo " Image: $image" echo " IP Address: $ip_address"
echo " Port: $port" echo " Port: $port"
echo " Data: $data_path" echo " Data: $data_path"
echo " Media: $media_path"
echo " Domain: $domain" echo " Domain: $domain"
echo "" echo ""
# Docker check
if ! command -v docker >/dev/null 2>&1; then
echo -e "Docker: ${RED}Not installed${NC}"
return
fi
# Container status
echo "Container:" echo "Container:"
local state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null) if lxc_running; then
if [ "$state" = "running" ]; then
echo -e " Status: ${GREEN}Running${NC}" echo -e " Status: ${GREEN}Running${NC}"
local uptime=$(docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null) local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}')
echo " Uptime: $uptime" echo " PID: $pid"
elif [ -n "$state" ]; then
echo -e " Status: ${YELLOW}$state${NC}" # Health check
local health=$(curl -s "http://${ip_address}:${port}/health" 2>/dev/null)
if [ "$health" = "Healthy" ]; then
echo -e " Health: ${GREEN}Healthy${NC}"
else
echo -e " Health: ${YELLOW}Starting...${NC}"
fi
elif lxc_exists; then
echo -e " Status: ${YELLOW}Stopped${NC}"
else else
echo -e " Status: ${RED}Not installed${NC}" echo -e " Status: ${RED}Not installed${NC}"
fi fi
echo "" echo ""
# Media paths # Storage
local paths=$(uci -q get ${CONFIG}.media.media_path)
if [ -n "$paths" ]; then
echo "Media Libraries:"
for p in $paths; do
if [ -d "$p" ]; then
local count=$(ls -1 "$p" 2>/dev/null | wc -l)
echo " $p ($count items)"
else
echo -e " $p ${RED}(not found)${NC}"
fi
done
echo ""
fi
# Integration status
echo "Integrations:"
local haproxy_enabled=$(uci_get network.haproxy)
if [ "$haproxy_enabled" = "1" ]; then
local vhost_exists=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1)
if [ -n "$vhost_exists" ]; then
echo -e " HAProxy: ${GREEN}Configured${NC} ($domain)"
else
echo -e " HAProxy: ${YELLOW}Enabled but not configured${NC}"
fi
else
echo " HAProxy: Disabled"
fi
local mesh_enabled=$(uci_get mesh.enabled)
if [ "$mesh_enabled" = "1" ]; then
echo -e " Mesh P2P: ${GREEN}Enabled${NC}"
else
echo " Mesh P2P: Disabled"
fi
local fw_wan=$(uci_get network.firewall_wan)
if [ "$fw_wan" = "1" ]; then
echo -e " Firewall: ${GREEN}WAN access on port $port${NC}"
else
echo " Firewall: LAN only"
fi
# Disk usage
if [ -d "$data_path" ]; then if [ -d "$data_path" ]; then
local disk=$(du -sh "$data_path" 2>/dev/null | cut -f1) local disk=$(du -sh "$data_path" 2>/dev/null | cut -f1)
echo ""
echo "Storage:" echo "Storage:"
echo " Data size: ${disk:-unknown}" echo " Data size: ${disk:-unknown}"
fi fi
@ -459,84 +296,21 @@ cmd_status() {
# Logs & Shell # Logs & Shell
# ============================================================================ # ============================================================================
cmd_logs() { docker logs "$@" "$CONTAINER" 2>&1; } cmd_logs() {
defaults
if [ -f "$data_path/config/log/log_$(date +%Y%m%d).log" ]; then
tail "${@:--100}" "$data_path/config/log/log_$(date +%Y%m%d).log"
else
ls -la "$data_path/config/log/" 2>/dev/null || echo "No logs found"
fi
}
cmd_shell() { cmd_shell() {
docker exec -it "$CONTAINER" /bin/bash 2>/dev/null || docker exec -it "$CONTAINER" /bin/sh if ! lxc_running; then
} error "Container not running"
# ============================================================================
# Backup / Restore
# ============================================================================
cmd_backup() {
local backup_file="${1:-/tmp/jellyfin-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
defaults
log "Creating backup..."
tar -czf "$backup_file" \
-C / \
etc/config/jellyfin \
"${data_path#/}/config" \
2>/dev/null
if [ -f "$backup_file" ]; then
local size=$(ls -lh "$backup_file" | awk '{print $5}')
log "Backup created: $backup_file ($size)"
else
error "Backup failed"
return 1 return 1
fi fi
} lxc-attach -n "$CONTAINER" -- /bin/bash 2>/dev/null || lxc-attach -n "$CONTAINER" -- /bin/sh
cmd_restore() {
local backup_file="$1"
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
echo "Usage: jellyfinctl restore <backup_file>"
return 1
fi
require_root
log "Restoring from $backup_file..."
/etc/init.d/jellyfin stop 2>/dev/null
tar -xzf "$backup_file" -C /
/etc/init.d/jellyfin start
log "Restore complete"
}
# ============================================================================
# Service Run (procd integration)
# ============================================================================
cmd_service_run() {
require_root
check_prereqs || exit 1
defaults
stop_container
local media_mounts
media_mounts=$(build_media_mounts)
local gpu_args
gpu_args=$(build_gpu_args)
local docker_args="--name $CONTAINER"
docker_args="$docker_args -p ${port}:8096"
docker_args="$docker_args -v ${data_path}/config:/config"
docker_args="$docker_args -v ${data_path}/cache:/cache"
docker_args="$docker_args -e TZ=${timezone}"
# shellcheck disable=SC2086
exec docker run --rm $docker_args $media_mounts $gpu_args "$image"
}
cmd_service_stop() {
require_root
stop_container
} }
# ============================================================================ # ============================================================================
@ -545,61 +319,42 @@ cmd_service_stop() {
show_help() { show_help() {
cat << EOF cat << EOF
Jellyfin Media Server Control v$VERSION Jellyfin Media Server Control v$VERSION (LXC)
Usage: jellyfinctl <command> [options] Usage: jellyfinctl <command> [options]
Commands: Commands:
install Install prerequisites, pull image, configure integrations install Install LXC container
uninstall Stop service, remove container and integrations start Start Jellyfin
check Run prerequisite checks stop Stop Jellyfin
update Pull latest image and restart restart Restart Jellyfin
status Show service and integration status status Show status
logs [-f] [--tail N] Show container logs logs [-f] [--tail N] Show logs
shell Open shell inside container shell Open shell inside container
configure-haproxy Configure/update HAProxy vhost configure-haproxy Configure HAProxy vhost
remove-haproxy Remove HAProxy configuration
configure-fw Configure firewall rules
remove-fw Remove firewall rules
register-mesh Register with mesh P2P network
unregister-mesh Unregister from mesh network
backup [file] Create configuration backup
restore <file> Restore from backup
service-run Internal: run container via procd
service-stop Internal: stop container
Examples: Examples:
jellyfinctl install jellyfinctl install
jellyfinctl start
jellyfinctl status jellyfinctl status
jellyfinctl logs --tail 100 jellyfinctl logs -100
jellyfinctl backup /tmp/jellyfin.tar.gz
jellyfinctl configure-haproxy
EOF EOF
} }
case "${1:-}" in case "${1:-}" in
install) shift; cmd_install "$@" ;; install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;; start) shift; cmd_start "$@" ;;
check) shift; cmd_check "$@" ;; stop) shift; cmd_stop "$@" ;;
update) shift; cmd_update "$@" ;; restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;; status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;; logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;; shell) shift; cmd_shell "$@" ;;
configure-haproxy) configure_haproxy ;; configure-haproxy) configure_haproxy ;;
remove-haproxy) remove_haproxy ;; service-run) cmd_start ;;
configure-fw) configure_firewall ;; service-stop) cmd_stop ;;
remove-fw) remove_firewall ;;
register-mesh) register_mesh_service ;;
unregister-mesh) unregister_mesh_service ;;
backup) shift; cmd_backup "$@" ;;
restore) shift; cmd_restore "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') show_help ;; help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;; *) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac esac

View File

@ -1,39 +1,34 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-mailserver PKG_NAME:=secubox-app-mailserver
PKG_VERSION:=1.0.0 PKG_VERSION:=2.0.0
PKG_RELEASE:=2 PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team PKG_MAINTAINER:=SecuBox <info@secubox.in>
PKG_LICENSE:=MIT PKG_LICENSE:=AGPL-3.0
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME) define Package/$(PKG_NAME)
SECTION:=secubox SECTION:=secubox
CATEGORY:=SecuBox CATEGORY:=SecuBox
TITLE:=SecuBox Mail Server Manager SUBMENU:=Apps
DEPENDS:=+lxc +secubox-app-dns-provider TITLE:=SecuBox Mail Server (LXC)
DEPENDS:=+lxc +curl +openssl-util
PKGARCH:=all PKGARCH:=all
endef endef
define Package/$(PKG_NAME)/description define Package/$(PKG_NAME)/description
Custom mail server (Postfix + Dovecot) in LXC with mesh backup support. Postfix + Dovecot mail server running in LXC container.
Integrates with dnsctl for MX/SPF/DKIM/DMARC management. Supports IMAP/SMTP with SSL/TLS.
endef
define Build/Compile
endef endef
define Package/$(PKG_NAME)/install define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/mailserver $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/mailserver $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/mailctl $(1)/usr/sbin/ $(INSTALL_BIN) ./files/usr/sbin/mailserverctl $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/lib/mailserver
$(INSTALL_DATA) ./files/usr/lib/mailserver/*.sh $(1)/usr/lib/mailserver/
endef endef
$(eval $(call BuildPackage,$(PKG_NAME))) $(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,570 @@
#!/bin/sh
# SecuBox Mailserver Controller
# LXC-based Postfix + Dovecot mail server
VERSION="2.0.0"
CONFIG="mailserver"
CONTAINER="mailserver"
LXC_PATH="/srv/lxc/mailserver"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[MAILSERVER]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# Configuration Helpers
# ============================================================================
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci -q set ${CONFIG}.$1="$2"; }
require_root() {
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
}
defaults() {
ip_address="$(uci_get main.ip_address)"
[ -z "$ip_address" ] && ip_address="192.168.255.30"
domain="$(uci_get main.domain)"
[ -z "$domain" ] && domain="secubox.in"
hostname="$(uci_get main.hostname)"
[ -z "$hostname" ] && hostname="mail.$domain"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
# ============================================================================
# LXC Helpers
# ============================================================================
lxc_running() {
lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING"
}
lxc_exists() {
[ -d "$LXC_PATH/rootfs" ]
}
create_lxc_config() {
defaults
cat > "$LXC_PATH/config" << EOF
lxc.uts.name = mailserver
lxc.rootfs.path = dir:${LXC_PATH}/rootfs
lxc.net.0.type = veth
lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.ipv4.address = ${ip_address}/24
lxc.net.0.ipv4.gateway = 192.168.255.1
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.seccomp.profile =
lxc.tty.max = 0
lxc.pty.max = 256
lxc.cgroup2.memory.max = 512000000
lxc.init.cmd = /opt/start-mail.sh
EOF
}
create_startup_script() {
cat > "$LXC_PATH/rootfs/opt/start-mail.sh" << 'EOF'
#!/bin/sh
# Mailserver startup script
# Start services
/usr/sbin/rsyslogd
sleep 1
/usr/sbin/postfix start
/usr/sbin/dovecot
echo "Mail services started"
# Keep container running
exec tail -f /var/log/dovecot.log /var/log/messages 2>/dev/null || exec sleep infinity
EOF
chmod +x "$LXC_PATH/rootfs/opt/start-mail.sh"
}
# ============================================================================
# Alpine Bootstrap
# ============================================================================
bootstrap_alpine() {
require_root
defaults
log "Bootstrapping Alpine Linux rootfs..."
ensure_dir "$LXC_PATH"
# Download apk-tools-static
cd "$LXC_PATH"
if [ ! -f sbin/apk.static ]; then
log "Downloading apk-tools-static..."
curl -L -o apk-tools-static.apk \
"https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/apk-tools-static-2.14.6-r3.apk"
tar -xzf apk-tools-static.apk sbin/apk.static
rm -f apk-tools-static.apk
fi
# Bootstrap rootfs
log "Installing base system..."
./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \
-U --allow-untrusted --root rootfs --initdb add \
alpine-base alpine-baselayout busybox musl openrc
# Set up repositories
mkdir -p rootfs/etc/apk
cat > rootfs/etc/apk/repositories << 'REPOEOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
REPOEOF
# Set up DNS
cat > rootfs/etc/resolv.conf << 'DNSEOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
DNSEOF
# Set hostname
echo "mailserver" > rootfs/etc/hostname
log "Base system installed"
}
install_mail_packages() {
require_root
if ! lxc_running; then
log "Starting container for package installation..."
lxc-start -n "$CONTAINER" -d
sleep 3
fi
log "Installing mail packages..."
lxc-attach -n "$CONTAINER" -- apk update
lxc-attach -n "$CONTAINER" -- apk add --no-cache \
postfix \
dovecot \
dovecot-lmtpd \
ca-certificates \
openssl \
rsyslog
log "Packages installed"
}
# ============================================================================
# Mail Configuration
# ============================================================================
configure_postfix() {
defaults
local rootfs="$LXC_PATH/rootfs"
log "Configuring Postfix..."
cat > "$rootfs/etc/postfix/main.cf" << EOF
# Basic config
myhostname = $hostname
mydomain = $domain
myorigin = \$mydomain
mydestination = \$myhostname, localhost.\$mydomain, localhost
mynetworks = 127.0.0.0/8 [::1]/128 192.168.255.0/24
# Virtual mailbox
virtual_mailbox_domains = $domain
virtual_mailbox_base = /var/mail
virtual_mailbox_maps = hash:/etc/postfix/vmailbox
virtual_uid_maps = static:102
virtual_gid_maps = static:105
virtual_transport = lmtp:unix:private/dovecot-lmtp
# SASL auth via Dovecot
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = \$mydomain
broken_sasl_auth_clients = yes
# TLS
smtpd_tls_cert_file = /etc/ssl/certs/mail.crt
smtpd_tls_key_file = /etc/ssl/private/mail.key
smtpd_tls_security_level = may
smtp_tls_security_level = may
# Restrictions
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks
# Limits
mailbox_size_limit = 0
message_size_limit = 52428800
inet_interfaces = all
inet_protocols = ipv4
EOF
cat > "$rootfs/etc/postfix/master.cf" << 'EOF'
smtp inet n - n - - smtpd
submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
smtps inet n - n - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
EOF
log "Postfix configured"
}
configure_dovecot() {
defaults
local rootfs="$LXC_PATH/rootfs"
log "Configuring Dovecot..."
cat > "$rootfs/etc/dovecot/dovecot.conf" << 'EOF'
protocols = imap lmtp
listen = *
mail_location = maildir:/var/mail/%d/%n
mail_uid = 102
mail_gid = 105
first_valid_uid = 102
last_valid_uid = 102
# Auth
auth_mechanisms = plain login
passdb {
driver = passwd-file
args = /etc/dovecot/users
}
userdb {
driver = static
args = uid=102 gid=105 home=/var/mail/%d/%n
}
# SSL
ssl = yes
ssl_cert = </etc/ssl/certs/mail.crt
ssl_key = </etc/ssl/private/mail.key
# Services
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
namespace inbox {
inbox = yes
separator = /
}
log_path = /var/log/dovecot.log
info_log_path = /var/log/dovecot-info.log
EOF
log "Dovecot configured"
}
generate_ssl_cert() {
local rootfs="$LXC_PATH/rootfs"
defaults
log "Generating SSL certificate..."
mkdir -p "$rootfs/etc/ssl/private" "$rootfs/etc/ssl/certs"
openssl req -x509 -nodes -days 3650 \
-newkey rsa:2048 \
-keyout "$rootfs/etc/ssl/private/mail.key" \
-out "$rootfs/etc/ssl/certs/mail.crt" \
-subj "/CN=$hostname/O=SecuBox/C=FR" 2>/dev/null
chmod 600 "$rootfs/etc/ssl/private/mail.key"
log "SSL certificate generated"
}
# ============================================================================
# User Management
# ============================================================================
cmd_add_user() {
local email="$1"
local password="$2"
if [ -z "$email" ] || [ -z "$password" ]; then
echo "Usage: mailserverctl add-user <email> <password>"
return 1
fi
local user=$(echo "$email" | cut -d@ -f1)
local domain=$(echo "$email" | cut -d@ -f2)
local rootfs="$LXC_PATH/rootfs"
# Create mailbox entry
echo "$email ${domain}/${user}/" >> "$rootfs/etc/postfix/vmailbox"
# Generate password hash and add to users file
if lxc_running; then
local pass_hash=$(lxc-attach -n "$CONTAINER" -- doveadm pw -s SHA512-CRYPT -p "$password")
echo "${email}:${pass_hash}:102:105::/var/mail/${domain}/${user}::" >> "$rootfs/etc/dovecot/users"
else
error "Container not running. Start it first."
return 1
fi
# Create mail directory
mkdir -p "$rootfs/var/mail/${domain}/${user}"
lxc-attach -n "$CONTAINER" -- chown -R vmail:vmail "/var/mail/${domain}"
# Rebuild postfix maps
lxc-attach -n "$CONTAINER" -- postmap /etc/postfix/vmailbox
log "User $email added"
}
# ============================================================================
# Service Control
# ============================================================================
cmd_install() {
require_root
log "Installing Mailserver LXC..."
defaults
if lxc_exists; then
log "Container already exists"
else
bootstrap_alpine
fi
create_lxc_config
create_startup_script
# Start for package installation
lxc-start -n "$CONTAINER" -d
sleep 3
if lxc_running; then
install_mail_packages
lxc-stop -n "$CONTAINER"
fi
configure_postfix
configure_dovecot
generate_ssl_cert
# Create vmail user directories
local rootfs="$LXC_PATH/rootfs"
mkdir -p "$rootfs/var/mail"
mkdir -p "$rootfs/var/spool/postfix/private"
# Create minimal interfaces file
cat > "$rootfs/etc/network/interfaces" << 'EOF'
auto lo
iface lo inet loopback
EOF
uci_set main.enabled '1'
uci commit ${CONFIG}
log "Mailserver installed!"
log "Add users with: mailserverctl add-user user@domain.com password"
log "Start with: mailserverctl start"
}
cmd_start() {
require_root
if lxc_running; then
log "Mailserver already running"
return 0
fi
if ! lxc_exists; then
error "Mailserver not installed. Run 'mailserverctl install' first"
return 1
fi
defaults
create_lxc_config
log "Starting Mailserver LXC..."
lxc-start -n "$CONTAINER" -d
sleep 5
if lxc_running; then
log "Mailserver started at $ip_address"
log "IMAP: ${ip_address}:993 (SSL)"
log "SMTP: ${ip_address}:465 (SSL)"
else
error "Failed to start Mailserver"
return 1
fi
}
cmd_stop() {
require_root
if ! lxc_running; then
log "Mailserver is not running"
return 0
fi
log "Stopping Mailserver..."
lxc-stop -n "$CONTAINER"
log "Mailserver stopped"
}
cmd_restart() {
cmd_stop
sleep 2
cmd_start
}
cmd_status() {
defaults
echo ""
echo "========================================"
echo " SecuBox Mailserver v$VERSION (LXC)"
echo "========================================"
echo ""
echo "Configuration:"
echo " Domain: $domain"
echo " Hostname: $hostname"
echo " IP Address: $ip_address"
echo ""
echo "Container:"
if lxc_running; then
echo -e " Status: ${GREEN}Running${NC}"
local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}')
echo " PID: $pid"
# Test IMAP
local imap_test=$(echo "a LOGOUT" | openssl s_client -connect ${ip_address}:993 -quiet 2>/dev/null | head -1)
if echo "$imap_test" | grep -q "OK"; then
echo -e " IMAP: ${GREEN}OK${NC}"
else
echo -e " IMAP: ${YELLOW}Not responding${NC}"
fi
elif lxc_exists; then
echo -e " Status: ${YELLOW}Stopped${NC}"
else
echo -e " Status: ${RED}Not installed${NC}"
fi
echo ""
echo "Ports:"
echo " SMTP: 25, 465 (SSL), 587 (submission)"
echo " IMAP: 143, 993 (SSL)"
echo ""
}
cmd_shell() {
if ! lxc_running; then
error "Container not running"
return 1
fi
lxc-attach -n "$CONTAINER" -- /bin/sh
}
# ============================================================================
# Main
# ============================================================================
show_help() {
cat << EOF
SecuBox Mailserver Control v$VERSION (LXC)
Usage: mailserverctl <command> [options]
Commands:
install Install LXC mail server
start Start mail server
stop Stop mail server
restart Restart mail server
status Show status
add-user <email> <pass> Add mail user
shell Open shell in container
Examples:
mailserverctl install
mailserverctl add-user admin@example.com MyPassword123
mailserverctl start
EOF
}
case "${1:-}" in
install) shift; cmd_install "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
add-user) shift; cmd_add_user "$@" ;;
shell) shift; cmd_shell "$@" ;;
service-run) cmd_start ;;
service-stop) cmd_stop ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac
exit 0

View File

@ -0,0 +1,34 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-roundcube
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox <info@secubox.in>
PKG_LICENSE:=AGPL-3.0
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-roundcube
SECTION:=secubox
CATEGORY:=SecuBox
SUBMENU:=Apps
TITLE:=Roundcube Webmail (LXC)
DEPENDS:=+lxc +curl
PKGARCH:=all
endef
define Package/secubox-app-roundcube/description
Roundcube Webmail running in LXC container with nginx and PHP-FPM.
Provides web-based email client for IMAP servers.
endef
define Build/Compile
endef
define Package/secubox-app-roundcube/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/roundcubectl $(1)/usr/sbin/
endef
$(eval $(call BuildPackage,secubox-app-roundcube))

View File

@ -0,0 +1,437 @@
#!/bin/sh
# SecuBox Roundcube Webmail Controller
# LXC-based nginx + PHP-FPM + Roundcube
VERSION="1.0.0"
CONFIG="roundcube"
CONTAINER="roundcube"
LXC_PATH="/srv/lxc/roundcube"
ROUNDCUBE_VERSION="1.6.12"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[ROUNDCUBE]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# Configuration
# ============================================================================
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci -q set ${CONFIG}.$1="$2"; }
require_root() {
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
}
defaults() {
port="$(uci_get main.port)"
[ -z "$port" ] && port="8027"
mail_host="$(uci_get main.mail_host)"
[ -z "$mail_host" ] && mail_host="192.168.255.30"
domain="$(uci_get main.domain)"
[ -z "$domain" ] && domain="webmail.gk2.secubox.in"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
# ============================================================================
# LXC Helpers
# ============================================================================
lxc_running() {
lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING"
}
lxc_exists() {
[ -d "$LXC_PATH/rootfs" ]
}
create_lxc_config() {
defaults
cat > "$LXC_PATH/config" << EOF
lxc.uts.name = roundcube
lxc.rootfs.path = dir:${LXC_PATH}/rootfs
lxc.net.0.type = none
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.seccomp.profile =
lxc.tty.max = 0
lxc.pty.max = 256
lxc.cgroup2.memory.max = 128000000
lxc.init.cmd = /opt/start-roundcube.sh
EOF
}
create_startup_script() {
cat > "$LXC_PATH/rootfs/opt/start-roundcube.sh" << 'EOF'
#!/bin/sh
set -e
# Initialize SQLite database if not exists
if [ ! -f /var/www/roundcube/db/roundcube.db ]; then
echo "Initializing Roundcube database..."
mkdir -p /var/www/roundcube/db
cd /var/www/roundcube
sqlite3 db/roundcube.db < SQL/sqlite.initial.sql
chown nginx:nginx db/roundcube.db
chmod 640 db/roundcube.db
echo "Database initialized"
fi
# Start PHP-FPM
echo "Starting PHP-FPM..."
php-fpm84 -D
# Start nginx in foreground
echo "Starting nginx..."
exec nginx -g "daemon off;"
EOF
chmod +x "$LXC_PATH/rootfs/opt/start-roundcube.sh"
}
# ============================================================================
# Installation
# ============================================================================
bootstrap_alpine() {
require_root
log "Bootstrapping Alpine Linux rootfs..."
ensure_dir "$LXC_PATH"
cd "$LXC_PATH"
if [ ! -f sbin/apk.static ]; then
log "Downloading apk-tools-static..."
curl -L -o apk-tools-static.apk \
"https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/apk-tools-static-2.14.6-r3.apk"
tar -xzf apk-tools-static.apk sbin/apk.static
rm -f apk-tools-static.apk
fi
log "Installing base system..."
./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \
-U --allow-untrusted --root rootfs --initdb add \
alpine-base alpine-baselayout busybox musl
mkdir -p rootfs/etc/apk
cat > rootfs/etc/apk/repositories << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF
cat > rootfs/etc/resolv.conf << 'EOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF
log "Base system installed"
}
install_packages() {
require_root
if ! lxc_running; then
log "Starting container for package installation..."
lxc-start -n "$CONTAINER" -d
sleep 3
fi
log "Installing packages..."
lxc-attach -n "$CONTAINER" -- apk update
lxc-attach -n "$CONTAINER" -- apk add --no-cache \
nginx \
php84 php84-fpm php84-imap php84-mbstring php84-openssl \
php84-session php84-pdo php84-pdo_sqlite php84-sqlite3 \
php84-xml php84-dom php84-intl php84-zip php84-gd \
php84-ctype php84-json php84-fileinfo php84-ldap \
sqlite curl
log "Packages installed"
}
download_roundcube() {
local rootfs="$LXC_PATH/rootfs"
log "Downloading Roundcube $ROUNDCUBE_VERSION..."
mkdir -p "$rootfs/var/www"
curl -L -o "$rootfs/tmp/roundcube.tar.gz" \
"https://github.com/roundcube/roundcubemail/releases/download/${ROUNDCUBE_VERSION}/roundcubemail-${ROUNDCUBE_VERSION}-complete.tar.gz"
tar -xzf "$rootfs/tmp/roundcube.tar.gz" -C "$rootfs/var/www/"
mv "$rootfs/var/www/roundcubemail-${ROUNDCUBE_VERSION}" "$rootfs/var/www/roundcube"
rm -f "$rootfs/tmp/roundcube.tar.gz"
log "Roundcube downloaded"
}
configure_roundcube() {
defaults
local rootfs="$LXC_PATH/rootfs"
log "Configuring Roundcube..."
cat > "$rootfs/var/www/roundcube/config/config.inc.php" << EOF
<?php
\$config["db_dsnw"] = "sqlite:////var/www/roundcube/db/roundcube.db?mode=0640";
\$config["imap_host"] = "ssl://${mail_host}:993";
\$config["smtp_host"] = "ssl://${mail_host}:465";
\$config["imap_conn_options"] = [
"ssl" => ["verify_peer" => false, "verify_peer_name" => false]
];
\$config["smtp_conn_options"] = [
"ssl" => ["verify_peer" => false, "verify_peer_name" => false]
];
\$config["support_url"] = "";
\$config["product_name"] = "SecuBox Webmail";
\$config["des_key"] = "rcmail-!24ByteDESKey*Sym";
\$config["plugins"] = ["archive", "zipdownload"];
\$config["skin"] = "elastic";
\$config["language"] = "fr_FR";
EOF
# Configure nginx
cat > "$rootfs/etc/nginx/http.d/roundcube.conf" << EOF
server {
listen ${port};
server_name _;
root /var/www/roundcube;
index index.php;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php\$ {
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
location ~ /\. {
deny all;
}
}
EOF
# Configure PHP-FPM
mkdir -p "$rootfs/etc/php84/php-fpm.d"
cat > "$rootfs/etc/php84/php-fpm.d/www.conf" << 'EOF'
[www]
user = nginx
group = nginx
listen = /run/php-fpm.sock
listen.owner = nginx
listen.group = nginx
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
EOF
# Set permissions
lxc-attach -n "$CONTAINER" -- chown -R nginx:nginx /var/www/roundcube 2>/dev/null || true
log "Roundcube configured"
}
configure_haproxy() {
defaults
if ! command -v haproxyctl >/dev/null 2>&1; then
warn "haproxyctl not found"
return 0
fi
local vhost_name=$(echo "$domain" | tr '.' '_')
if ! uci -q get haproxy.roundcube >/dev/null 2>&1; then
log "Creating HAProxy backend..."
uci set haproxy.roundcube=backend
uci set haproxy.roundcube.name='roundcube'
uci set haproxy.roundcube.mode='http'
uci set haproxy.roundcube.balance='roundrobin'
uci set haproxy.roundcube.enabled='1'
uci set haproxy.roundcube.option='forwardfor'
uci add_list haproxy.roundcube.http_request='set-header X-Forwarded-Proto https'
uci add_list haproxy.roundcube.http_request='set-header X-Real-IP %[src]'
uci set haproxy.roundcube_srv=server
uci set haproxy.roundcube_srv.backend='roundcube'
uci set haproxy.roundcube_srv.name='roundcube'
uci set haproxy.roundcube_srv.address='192.168.255.1'
uci set haproxy.roundcube_srv.port="$port"
uci set haproxy.roundcube_srv.weight='100'
uci set haproxy.roundcube_srv.check='1'
uci set haproxy.roundcube_srv.enabled='1'
fi
if ! uci -q get haproxy.${vhost_name} >/dev/null 2>&1; then
log "Creating HAProxy vhost for $domain..."
uci set haproxy.${vhost_name}=vhost
uci set haproxy.${vhost_name}.domain="$domain"
uci set haproxy.${vhost_name}.backend='roundcube'
uci set haproxy.${vhost_name}.ssl='1'
uci set haproxy.${vhost_name}.ssl_redirect='1'
uci set haproxy.${vhost_name}.acme='1'
uci set haproxy.${vhost_name}.waf_bypass='1'
uci set haproxy.${vhost_name}.enabled='1'
fi
uci commit haproxy
haproxyctl generate 2>/dev/null
haproxyctl reload 2>/dev/null
log "HAProxy configured for $domain"
}
cmd_install() {
require_root
log "Installing Roundcube LXC..."
defaults
if ! lxc_exists; then
bootstrap_alpine
fi
create_lxc_config
create_startup_script
lxc-start -n "$CONTAINER" -d
sleep 3
if lxc_running; then
install_packages
download_roundcube
configure_roundcube
lxc-stop -n "$CONTAINER"
else
error "Failed to start container"
return 1
fi
configure_haproxy
uci_set main.enabled '1'
uci commit ${CONFIG}
log "Roundcube installed!"
log "Start with: roundcubectl start"
}
cmd_start() {
require_root
if lxc_running; then
log "Roundcube already running"
return 0
fi
if ! lxc_exists; then
error "Roundcube not installed"
return 1
fi
defaults
create_lxc_config
log "Starting Roundcube LXC..."
lxc-start -n "$CONTAINER" -d
sleep 3
if lxc_running; then
log "Roundcube started on port $port"
else
error "Failed to start Roundcube"
return 1
fi
}
cmd_stop() {
require_root
if ! lxc_running; then
log "Roundcube is not running"
return 0
fi
log "Stopping Roundcube..."
lxc-stop -n "$CONTAINER"
log "Roundcube stopped"
}
cmd_restart() {
cmd_stop
sleep 2
cmd_start
}
cmd_status() {
defaults
echo ""
echo "========================================"
echo " SecuBox Roundcube v$VERSION (LXC)"
echo "========================================"
echo ""
echo "Configuration:"
echo " Port: $port"
echo " Mail Host: $mail_host"
echo " Domain: $domain"
echo ""
echo "Container:"
if lxc_running; then
echo -e " Status: ${GREEN}Running${NC}"
local test=$(curl -sI "http://127.0.0.1:$port/" 2>/dev/null | head -1)
if echo "$test" | grep -q "200"; then
echo -e " Web: ${GREEN}OK${NC}"
else
echo -e " Web: ${YELLOW}Starting...${NC}"
fi
elif lxc_exists; then
echo -e " Status: ${YELLOW}Stopped${NC}"
else
echo -e " Status: ${RED}Not installed${NC}"
fi
echo ""
}
show_help() {
cat << EOF
SecuBox Roundcube Webmail v$VERSION (LXC)
Usage: roundcubectl <command>
Commands:
install Install LXC container
start Start Roundcube
stop Stop Roundcube
restart Restart Roundcube
status Show status
EOF
}
case "${1:-}" in
install) shift; cmd_install "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
service-run) cmd_start ;;
service-stop) cmd_stop ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac
exit 0