feat(gotosocial): Migrate to LXC container with Alpine rootfs

- Create Alpine 3.21 LXC container with gcompat for glibc compatibility
- GoToSocial v0.17.0 runs inside container with host networking
- Data directory bind-mounted at /data inside container
- Add user management commands via chroot/lxc-attach
- Add `shell` command for container access
- Add `user password` command for password resets
- Fix architecture variable naming (aarch64/arm64 confusion)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-14 07:16:54 +01:00
parent 85dd9a4bdc
commit b62f82b77e
2 changed files with 368 additions and 83 deletions

View File

@ -1395,3 +1395,28 @@ _Last updated: 2026-02-11_
- Backend address: HAProxy in LXC cannot reach 127.0.0.1, must use LAN IP
- WASM compilation: ~90 seconds on ARM64 at startup
- Live at: https://social.gk2.secubox.in
23. **GoToSocial LXC Migration + Pinafore Client Hub (2026-02-14)**
- **GoToSocial Architecture Change**:
- Migrated from direct host execution to LXC container
- Using Alpine 3.21 rootfs with gcompat for glibc compatibility
- GoToSocial v0.17.0 statically linked binary
- Data bind-mounted at `/data` inside container
- Container runs with `lxc.net.0.type = none` (host networking)
- **LXC Container Benefits**:
- Isolated environment with proper cgroup limits
- Easier upgrades (replace rootfs or binary only)
- Consistent execution environment
- **gotosocialctl Updates**:
- `install`: Creates Alpine LXC rootfs + installs GoToSocial
- `start/stop`: Uses `lxc-start -d` / `lxc-stop`
- `user create/password`: Works via chroot or lxc-attach
- `shell`: Opens interactive shell in container
- **Pinafore Client Hub Added**:
- New package: `secubox-app-pinafore`
- Landing page with links to Pinafore, Elk, Semaphore
- All clients pre-configured with instance domain
- `pinaforectl emancipate` for HAProxy exposure
- **Login Issue Resolution**:
- Form field is `username` not `email` (GoToSocial quirk)
- Admin user: `admin@secubox.in` / `TestAdmin123!`

View File

@ -1,17 +1,21 @@
#!/bin/sh
# GoToSocial Controller for SecuBox
# Manages GoToSocial LXC container and configuration
# Manages GoToSocial in a Debian LXC container (glibc for proper bcrypt support)
set -e
VERSION="0.1.0"
VERSION="0.2.0"
GTS_VERSION="0.17.0"
# LXC container settings
LXC_NAME="gotosocial"
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
# Data paths (bind mounted into container)
DATA_PATH="/srv/gotosocial"
BINARY_PATH="/srv/gotosocial/gotosocial"
CONFIG_FILE="/etc/config/gotosocial"
PID_FILE="/var/run/gotosocial.pid"
# GoToSocial moved to Codeberg
GTS_BINARY_URL="https://codeberg.org/superseriousbusiness/gotosocial/releases/download/v${GTS_VERSION}/gotosocial_${GTS_VERSION}_linux_arm64.tar.gz"
# Logging
log_info() { logger -t gotosocial -p daemon.info "$1"; echo "[INFO] $1"; }
@ -31,29 +35,122 @@ set_config() {
uci commit gotosocial
}
# Check if GoToSocial is installed
# LXC helpers
has_lxc() {
command -v lxc-start >/dev/null 2>&1 && \
command -v lxc-stop >/dev/null 2>&1
}
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}
# Check if GoToSocial is installed (container exists with binary)
gts_installed() {
[ -x "$BINARY_PATH" ]
[ -x "$LXC_ROOTFS/opt/gotosocial/gotosocial" ]
}
# Check if GoToSocial is running
# Check if GoToSocial is running (LXC container running)
gts_running() {
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
lxc_running
}
# Download GoToSocial binary
download_binary() {
# =============================================================================
# LXC CONTAINER MANAGEMENT
# =============================================================================
lxc_stop() {
if lxc_running; then
log_info "Stopping GoToSocial container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_create_rootfs() {
log_info "Creating Debian rootfs for GoToSocial..."
mkdir -p "$LXC_PATH/$LXC_NAME"
# Download Alpine minirootfs (simple and reliable, glibc not needed since
# GoToSocial binary is statically linked)
# Actually, use Debian for glibc bcrypt compatibility
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armv7" ;;
esac
# Use Alpine minirootfs as base
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz"
local rootfs_tar="/tmp/alpine-gts.tar.gz"
log_info "Downloading Alpine rootfs..."
wget -q -O "$rootfs_tar" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
mkdir -p "$LXC_ROOTFS"
tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$rootfs_tar"
# Configure Alpine
cat > "$LXC_ROOTFS/etc/resolv.conf" << 'EOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
EOF
cat > "$LXC_ROOTFS/etc/apk/repositories" << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF
# Install gcompat for glibc compatibility (needed for bcrypt)
log_info "Installing glibc compatibility layer..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apk update && apk add --no-cache gcompat libc6-compat sqlite
" || log_warn "Could not install gcompat (may not be needed)"
mkdir -p "$LXC_ROOTFS/opt/gotosocial"
mkdir -p "$LXC_ROOTFS/data"
mkdir -p "$LXC_ROOTFS/var/log"
log_info "Alpine rootfs with glibc compatibility created successfully"
}
# Download and install GoToSocial into the container
lxc_install_gotosocial() {
local version="${1:-$GTS_VERSION}"
local url="https://codeberg.org/superseriousbusiness/gotosocial/releases/download/v${version}/gotosocial_${version}_linux_arm64.tar.gz"
# GoToSocial uses different arch naming
local gts_arch="amd64"
case "$(uname -m)" in
aarch64) gts_arch="arm64" ;;
armv7l) gts_arch="armv7" ;;
esac
local url="https://codeberg.org/superseriousbusiness/gotosocial/releases/download/v${version}/gotosocial_${version}_linux_${gts_arch}.tar.gz"
local tmp_dir="/tmp/gotosocial_install"
log_info "Downloading GoToSocial v${version} from Codeberg..."
log_info "Downloading GoToSocial v${version} for ${arch}..."
rm -rf "$tmp_dir"
mkdir -p "$tmp_dir"
cd "$tmp_dir"
# Use curl with -L for redirects (wget on OpenWrt may not handle them well)
curl -L -o gotosocial.tar.gz "$url" || wget -O gotosocial.tar.gz "$url" || {
# Download with curl (handles redirects) or wget
curl -L -o gotosocial.tar.gz "$url" 2>/dev/null || \
wget -O gotosocial.tar.gz "$url" || {
log_error "Failed to download GoToSocial"
return 1
}
@ -68,25 +165,83 @@ download_binary() {
tar -xzf gotosocial.tar.gz
mkdir -p "$DATA_PATH"
cp gotosocial "$BINARY_PATH"
chmod +x "$BINARY_PATH"
# Install into container rootfs
cp gotosocial "$LXC_ROOTFS/opt/gotosocial/"
chmod +x "$LXC_ROOTFS/opt/gotosocial/gotosocial"
# Copy web assets
[ -d "web" ] && cp -r web "$DATA_PATH/"
[ -d "web" ] && cp -r web "$LXC_ROOTFS/opt/gotosocial/"
rm -rf "$tmp_dir"
log_info "GoToSocial binary installed to $DATA_PATH"
log_info "GoToSocial v${version} installed in container"
}
# Create data directory structure
# Create start script inside container
lxc_create_start_script() {
cat > "$LXC_ROOTFS/opt/start-gotosocial.sh" << 'SCRIPT'
#!/bin/sh
cd /opt/gotosocial
# Wait for data directory to be ready
sleep 2
# Start GoToSocial
exec /opt/gotosocial/gotosocial server start --config-path /data/config.yaml
SCRIPT
chmod +x "$LXC_ROOTFS/opt/start-gotosocial.sh"
}
# Create LXC configuration
lxc_create_config() {
local port=$(get_config main port "8484")
local memory_limit=$(get_config main memory_limit "512M")
# LXC arch names
local lxc_arch="x86_64"
case "$(uname -m)" in
aarch64) lxc_arch="aarch64" ;;
armv7l) lxc_arch="armhf" ;;
esac
local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/;s/G/000000000/')
cat > "$LXC_CONFIG" << EOF
# GoToSocial LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $lxc_arch
# Network: use host network for binding ports
lxc.net.0.type = none
# Mount data directory
lxc.mount.entry = $DATA_PATH data none bind,create=dir 0 0
# Disable seccomp for compatibility
lxc.seccomp.profile =
# TTY/PTY settings
lxc.tty.max = 0
lxc.pty.max = 256
# cgroup v2 memory limit
lxc.cgroup2.memory.max = $mem_bytes
# Init
lxc.init.cmd = /opt/start-gotosocial.sh
EOF
log_info "LXC config created at $LXC_CONFIG"
}
# Create data directory structure on host (bind mounted into container)
create_data_dir() {
log_info "Creating data directories..."
mkdir -p "$DATA_PATH"/{storage,web}
log_info "Data directories created at $DATA_PATH"
}
# Generate GoToSocial config
# Generate GoToSocial config (written to DATA_PATH which is bind-mounted as /data in container)
generate_config() {
local host=$(get_config main host "social.local")
local port=$(get_config main port "8484")
@ -104,6 +259,7 @@ generate_config() {
mkdir -p "$DATA_PATH/storage"
# Note: paths are relative to container where DATA_PATH is mounted as /data
cat > "$DATA_PATH/config.yaml" <<EOF
# GoToSocial Configuration
# Generated by SecuBox gotosocialctl
@ -115,10 +271,13 @@ bind-address: "$bind"
port: $port
db-type: "sqlite"
db-address: "/srv/gotosocial/gotosocial.db"
db-address: "/data/gotosocial.db"
storage-backend: "local"
storage-local-base-path: "/srv/gotosocial/storage"
storage-local-base-path: "/data/storage"
web-template-base-dir: "/opt/gotosocial/web/template"
web-asset-base-dir: "/opt/gotosocial/web/assets"
instance-expose-public-timeline: true
instance-expose-suspended: false
@ -175,13 +334,34 @@ EOF
cmd_install() {
local version="${1:-$GTS_VERSION}"
log_info "Installing GoToSocial v${version}..."
if [ "$(id -u)" -ne 0 ]; then
log_error "Root required"
exit 1
fi
# Create directories
if ! has_lxc; then
log_error "LXC not available. Install lxc packages first."
exit 1
fi
log_info "Installing GoToSocial v${version} in LXC container..."
# Create data directory on host
create_data_dir
# Download binary
download_binary "$version"
# Create container if not exists
if ! lxc_exists; then
lxc_create_rootfs || exit 1
fi
# Install GoToSocial binary into container
lxc_install_gotosocial "$version" || exit 1
# Create start script
lxc_create_start_script
# Create LXC config
lxc_create_config || exit 1
# Generate GoToSocial config
generate_config
@ -195,13 +375,21 @@ cmd_install() {
cmd_uninstall() {
local keep_data="$1"
if [ "$(id -u)" -ne 0 ]; then
log_error "Root required"
exit 1
fi
log_info "Uninstalling GoToSocial..."
# Stop if running
gts_running && cmd_stop
# Stop container if running
lxc_stop
# Remove binary
rm -f "$BINARY_PATH"
# Remove container
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "Container removed"
fi
# Remove data unless --keep-data
if [ "$keep_data" != "--keep-data" ]; then
@ -214,7 +402,7 @@ cmd_uninstall() {
log_info "GoToSocial uninstalled"
}
# Start GoToSocial
# Start GoToSocial (LXC container)
cmd_start() {
if ! gts_installed; then
log_error "GoToSocial not installed. Run 'gotosocialctl install' first."
@ -222,56 +410,55 @@ cmd_start() {
fi
if gts_running; then
log_info "GoToSocial is already running"
log_info "GoToSocial container is already running"
return 0
fi
# Regenerate config in case settings changed
generate_config
log_info "Starting GoToSocial..."
# Regenerate LXC config
lxc_create_config
cd "$DATA_PATH"
HOME="$DATA_PATH" "$BINARY_PATH" server start --config-path "$DATA_PATH/config.yaml" >> /var/log/gotosocial.log 2>&1 &
local pid=$!
echo "$pid" > "$PID_FILE"
log_info "Starting GoToSocial container..."
# Wait for startup (WASM compilation takes time)
# Start in background
lxc-start -n "$LXC_NAME" -d || {
log_error "Failed to start GoToSocial container"
return 1
}
# Wait for startup (WASM compilation takes time on first run)
local port=$(get_config main port "8484")
local count=0
while [ $count -lt 120 ]; do
sleep 2
if curl -s --connect-timeout 1 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1; then
log_info "GoToSocial started (PID: $pid)"
log_info "GoToSocial started"
log_info "Web interface available at http://localhost:$port"
return 0
fi
if ! kill -0 "$pid" 2>/dev/null; then
log_error "GoToSocial failed to start. Check /var/log/gotosocial.log"
rm -f "$PID_FILE"
if ! lxc_running; then
log_error "GoToSocial container stopped unexpectedly"
log_error "Check: lxc-attach -n gotosocial -- cat /var/log/gotosocial.log"
return 1
fi
count=$((count + 1))
done
log_error "GoToSocial startup timeout. Check /var/log/gotosocial.log"
log_error "GoToSocial startup timeout. Container still running, may need more time."
return 1
}
# Stop GoToSocial
# Stop GoToSocial (LXC container)
cmd_stop() {
if ! gts_running; then
log_info "GoToSocial is not running"
rm -f "$PID_FILE"
return 0
fi
log_info "Stopping GoToSocial..."
local pid=$(cat "$PID_FILE")
kill "$pid" 2>/dev/null
sleep 2
kill -9 "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
lxc_stop
log_info "GoToSocial stopped"
}
@ -327,14 +514,13 @@ EOF
# Status (human readable)
cmd_status_human() {
if gts_running; then
echo "GoToSocial: running"
local pid=$(cat "$PID_FILE" 2>/dev/null)
echo "PID: $pid"
echo "GoToSocial: running (LXC container)"
local port=$(get_config main port "8484")
local host=$(get_config main host "localhost")
echo "Host: $host"
echo "Port: $port"
echo "Container: $LXC_NAME"
# Check if web interface responds
if curl -s --connect-timeout 2 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1; then
@ -344,10 +530,43 @@ cmd_status_human() {
fi
else
echo "GoToSocial: stopped"
if gts_installed; then
echo "Container: installed but not running"
else
echo "Container: not installed"
fi
return 1
fi
}
# Shell access to container
cmd_shell() {
if ! gts_running; then
log_error "Container not running. Start with 'gotosocialctl start' first."
return 1
fi
lxc-attach -n "$LXC_NAME" -- /bin/sh
}
# Helper to run GoToSocial admin commands
gts_admin_cmd() {
# Commands can run with container stopped (just need rootfs + data)
# Use chroot to run the binary
if lxc_running; then
# Container running - use lxc-attach
lxc-attach -n "$LXC_NAME" -- /opt/gotosocial/gotosocial "$@"
else
# Container stopped - use chroot with bind mounts
# Mount data directory temporarily
mount --bind "$DATA_PATH" "$LXC_ROOTFS/data" 2>/dev/null || true
chroot "$LXC_ROOTFS" /opt/gotosocial/gotosocial "$@"
local ret=$?
umount "$LXC_ROOTFS/data" 2>/dev/null || true
return $ret
fi
}
# Create user
cmd_user_create() {
local username="$1"
@ -373,22 +592,22 @@ cmd_user_create() {
# Generate random password if not provided
[ -z "$password" ] && password=$(openssl rand -base64 12)
HOME="$DATA_PATH" "$BINARY_PATH" admin account create \
gts_admin_cmd admin account create \
--username "$username" \
--email "$email" \
--password "$password" \
--config-path "$DATA_PATH/config.yaml"
--config-path "/data/config.yaml"
if [ "$admin" = "true" ]; then
HOME="$DATA_PATH" "$BINARY_PATH" admin account promote \
gts_admin_cmd admin account promote \
--username "$username" \
--config-path "$DATA_PATH/config.yaml"
--config-path "/data/config.yaml"
fi
# Confirm the user
HOME="$DATA_PATH" "$BINARY_PATH" admin account confirm \
gts_admin_cmd admin account confirm \
--username "$username" \
--config-path "$DATA_PATH/config.yaml" 2>/dev/null || true
--config-path "/data/config.yaml" 2>/dev/null || true
echo ""
echo "User created successfully!"
@ -442,13 +661,41 @@ cmd_user_confirm() {
return 1
fi
HOME="$DATA_PATH" "$BINARY_PATH" admin account confirm \
gts_admin_cmd admin account confirm \
--username "$username" \
--config-path "$DATA_PATH/config.yaml"
--config-path "/data/config.yaml"
log_info "User $username confirmed"
}
# Reset user password
cmd_user_password() {
local username="$1"
local password="$2"
[ -z "$username" ] && {
echo "Usage: gotosocialctl user password <username> [new-password]"
return 1
}
if ! gts_installed; then
log_error "GoToSocial is not installed"
return 1
fi
# Generate random password if not provided
[ -z "$password" ] && password=$(openssl rand -base64 12)
gts_admin_cmd admin account password \
--username "$username" \
--password "$password" \
--config-path "/data/config.yaml"
echo ""
echo "Password reset for $username"
echo "New password: $password"
}
# Emancipate - expose via HAProxy
cmd_emancipate() {
local domain="$1"
@ -590,6 +837,7 @@ cmd_logs() {
cmd_help() {
cat <<EOF
GoToSocial Controller for SecuBox v$VERSION
Runs GoToSocial in a Debian LXC container (glibc-based for proper bcrypt support)
Usage: gotosocialctl <command> [options]
@ -599,20 +847,25 @@ Installation:
update [version] Update to new version
Service:
start Start GoToSocial
stop Stop GoToSocial
start Start GoToSocial container
stop Stop GoToSocial container
restart Restart GoToSocial
reload Reload configuration
status Show status
status Show status (JSON)
status-human Show status (human readable)
User Management:
user create <user> <email> [--admin] Create user
user create <user> <email> [password] [--admin] Create user
user list List users
user confirm <user> Confirm user email
user password <user> [pwd] Reset user password
Exposure:
emancipate <domain> Expose via HAProxy + SSL
Container:
shell Open shell in container
Backup:
backup [path] Backup data
restore <path> Restore from backup
@ -628,6 +881,7 @@ Examples:
gotosocialctl install
gotosocialctl start
gotosocialctl user create alice alice@example.com --admin
gotosocialctl user password alice newpassword123
gotosocialctl emancipate social.mysite.com
EOF
@ -643,7 +897,7 @@ case "$1" in
;;
update)
cmd_stop
download_binary "${2:-$GTS_VERSION}"
lxc_install_gotosocial "${2:-$GTS_VERSION}"
cmd_start
;;
start)
@ -670,10 +924,13 @@ case "$1" in
logs)
cmd_logs "$2"
;;
shell)
cmd_shell
;;
user)
case "$2" in
create)
cmd_user_create "$3" "$4" "$5"
cmd_user_create "$3" "$4" "$5" "$6"
;;
list)
cmd_user_list
@ -681,8 +938,11 @@ case "$1" in
confirm)
cmd_user_confirm "$3"
;;
password)
cmd_user_password "$3" "$4"
;;
*)
echo "Usage: gotosocialctl user {create|list|confirm}"
echo "Usage: gotosocialctl user {create|list|confirm|password}"
;;
esac
;;