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

View File

@ -1,11 +1,11 @@
#!/bin/sh
# 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"
CONTAINER="secbx-jellyfin"
OPKG_UPDATED=0
CONTAINER="jellyfin"
LXC_PATH="/srv/lxc/jellyfin"
# Colors
RED='\033[0;31m'
@ -29,83 +29,68 @@ require_root() {
}
defaults() {
image="$(uci_get main.image)"
[ -z "$image" ] && image="jellyfin/jellyfin:latest"
data_path="$(uci_get main.data_path)"
[ -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)"
[ -z "$port" ] && port="8096"
timezone="$(uci_get main.timezone)"
[ -z "$timezone" ] && timezone="Europe/Paris"
hw_accel="$(uci_get transcoding.hw_accel)"
[ -z "$hw_accel" ] && hw_accel="0"
gpu_device="$(uci_get transcoding.gpu_device)"
ip_address="$(uci_get main.ip_address)"
[ -z "$ip_address" ] && ip_address="192.168.255.31"
domain="$(uci_get network.domain)"
[ -z "$domain" ] && domain="jellyfin.secubox.local"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
ensure_packages() {
for pkg in "$@"; do
if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1
OPKG_UPDATED=1
fi
opkg install "$pkg" || return 1
fi
done
# ============================================================================
# LXC Helpers
# ============================================================================
lxc_running() {
lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING"
}
# ============================================================================
# Prerequisite Checks
# ============================================================================
lxc_exists() {
[ -d "$LXC_PATH/rootfs" ]
}
check_prereqs() {
create_lxc_config() {
defaults
ensure_dir "$data_path"
[ -d /sys/fs/cgroup ] || { error "/sys/fs/cgroup missing"; return 1; }
ensure_packages dockerd docker containerd
/etc/init.d/dockerd enable >/dev/null 2>&1
/etc/init.d/dockerd start >/dev/null 2>&1
cat > "$LXC_PATH/config" << EOF
lxc.uts.name = jellyfin
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.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
}
# ============================================================================
# Docker Helpers
# ============================================================================
create_startup_script() {
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() {
defaults
docker pull "$image"
}
stop_container() {
docker stop "$CONTAINER" >/dev/null 2>&1 || true
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
echo "Starting Jellyfin..."
exec /jellyfin/jellyfin \
--datadir=/config \
--cachedir=/cache \
--webdir=/jellyfin/jellyfin-web
EOF
chmod +x "$LXC_PATH/rootfs/opt/start-jellyfin.sh"
}
# ============================================================================
@ -123,155 +108,39 @@ configure_haproxy() {
defaults
local ssl=$(uci_get network.haproxy_ssl)
local ssl_redirect=$(uci_get network.haproxy_ssl_redirect)
# Check if vhost already exists (idempotent)
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"
# Check if backend already exists
if uci -q get haproxy.jellyfin_web >/dev/null 2>&1; then
log "HAProxy backend jellyfin_web already exists, updating..."
uci set haproxy.jellyfin_web.server="media ${ip_address}:${port} weight 100 check"
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
local dns_enabled=$(uci -q get secubox-p2p.dns.enabled || echo "0")
if [ "$dns_enabled" = "1" ]; then
local dns_domain=$(uci -q get secubox-p2p.dns.base_domain || echo "mesh.local")
local hostname=$(echo "$domain" | cut -d'.' -f1)
log "Mesh DNS: $hostname.$dns_domain"
# Check if vhost already exists
local vhost_name=$(echo "$domain" | tr '.' '_')
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='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
}
unregister_mesh_service() {
if [ -x /usr/sbin/secubox-p2p ]; then
/usr/sbin/secubox-p2p unregister-service jellyfin 2>/dev/null
log "Unregistered Jellyfin from mesh network"
fi
uci commit haproxy
haproxyctl generate 2>/dev/null
haproxyctl reload 2>/dev/null
log "HAProxy configured for $domain -> ${ip_address}:${port}"
}
# ============================================================================
@ -280,83 +149,94 @@ unregister_mesh_service() {
cmd_install() {
require_root
log "Installing Jellyfin Media Server..."
log "Installing Jellyfin LXC container..."
check_prereqs || exit 1
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/cache"
log "Pulling Docker image..."
pull_image || exit 1
if [ ! -f "$LXC_PATH/rootfs/jellyfin/jellyfin" ]; then
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 commit ${CONFIG}
/etc/init.d/jellyfin enable
# Integrate with HAProxy if configured
configure_haproxy
# Configure firewall if WAN access requested
configure_firewall
# 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 ""
log "Jellyfin LXC installed successfully!"
log "Start with: jellyfinctl start"
}
cmd_uninstall() {
# ============================================================================
# Service Control
# ============================================================================
cmd_start() {
require_root
log "Uninstalling Jellyfin..."
/etc/init.d/jellyfin stop 2>/dev/null
/etc/init.d/jellyfin disable 2>/dev/null
stop_container
if lxc_running; then
log "Jellyfin already running"
return 0
fi
# Remove integrations
remove_haproxy
remove_firewall
unregister_mesh_service
if ! lxc_exists; then
error "Jellyfin not installed. Run 'jellyfinctl install' first"
return 1
fi
# Remove image
defaults
docker rmi "$image" 2>/dev/null
create_lxc_config
uci_set main.enabled '0'
uci commit ${CONFIG}
log "Starting Jellyfin LXC..."
lxc-start -n "$CONTAINER" -d
sleep 5
log "Jellyfin uninstalled. Data preserved at $data_path"
log "To remove data: rm -rf $data_path"
if lxc_running; then
log "Jellyfin started at http://${ip_address}:${port}"
else
error "Failed to start Jellyfin"
return 1
fi
}
# ============================================================================
# Check & Update
# ============================================================================
cmd_check() {
check_prereqs
echo "Prerequisite check completed."
}
cmd_update() {
cmd_stop() {
require_root
log "Pulling latest image..."
pull_image || exit 1
log "Restarting service..."
/etc/init.d/jellyfin restart
# Prune old images
docker image prune -f 2>/dev/null
log "Update complete"
if ! lxc_running; then
log "Jellyfin is not running"
return 0
fi
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 " Jellyfin Media Server v$VERSION"
echo " Jellyfin Media Server v$VERSION (LXC)"
echo "========================================"
echo ""
local enabled=$(uci_get main.enabled)
echo "Configuration:"
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 " Data: $data_path"
echo " Media: $media_path"
echo " Domain: $domain"
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:"
local state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null)
if [ "$state" = "running" ]; then
if lxc_running; then
echo -e " Status: ${GREEN}Running${NC}"
local uptime=$(docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null)
echo " Uptime: $uptime"
elif [ -n "$state" ]; then
echo -e " Status: ${YELLOW}$state${NC}"
local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}')
echo " PID: $pid"
# 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
echo -e " Status: ${RED}Not installed${NC}"
fi
echo ""
# Media paths
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
# Storage
if [ -d "$data_path" ]; then
local disk=$(du -sh "$data_path" 2>/dev/null | cut -f1)
echo ""
echo "Storage:"
echo " Data size: ${disk:-unknown}"
fi
@ -459,84 +296,21 @@ cmd_status() {
# 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() {
docker exec -it "$CONTAINER" /bin/bash 2>/dev/null || docker exec -it "$CONTAINER" /bin/sh
}
# ============================================================================
# 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"
if ! lxc_running; then
error "Container not running"
return 1
fi
}
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
lxc-attach -n "$CONTAINER" -- /bin/bash 2>/dev/null || lxc-attach -n "$CONTAINER" -- /bin/sh
}
# ============================================================================
@ -545,61 +319,42 @@ cmd_service_stop() {
show_help() {
cat << EOF
Jellyfin Media Server Control v$VERSION
Jellyfin Media Server Control v$VERSION (LXC)
Usage: jellyfinctl <command> [options]
Commands:
install Install prerequisites, pull image, configure integrations
uninstall Stop service, remove container and integrations
check Run prerequisite checks
update Pull latest image and restart
status Show service and integration status
install Install LXC container
start Start Jellyfin
stop Stop Jellyfin
restart Restart Jellyfin
status Show status
logs [-f] [--tail N] Show container logs
logs [-f] [--tail N] Show logs
shell Open shell inside container
configure-haproxy Configure/update 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
configure-haproxy Configure HAProxy vhost
Examples:
jellyfinctl install
jellyfinctl start
jellyfinctl status
jellyfinctl logs --tail 100
jellyfinctl backup /tmp/jellyfin.tar.gz
jellyfinctl configure-haproxy
jellyfinctl logs -100
EOF
}
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
configure-haproxy) configure_haproxy ;;
remove-haproxy) remove_haproxy ;;
configure-fw) configure_firewall ;;
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 "$@" ;;
service-run) cmd_start ;;
service-stop) cmd_stop ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac

View File

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