Files
homelab-optimized/deployments/mastodon/install.sh
Gitea Mirror Bot e71c8ddb4b
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m5s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-20 01:24:42 UTC
2026-04-20 01:24:42 +00:00

724 lines
20 KiB
Bash

#!/bin/bash
# =============================================================================
# Mastodon Production Installer
# =============================================================================
# Self-hosted Mastodon instance - production ready with Docker
#
# Supported: Ubuntu, Debian, Fedora, Rocky/Alma/RHEL 8+, Arch, openSUSE
# Deploys via Docker Compose
#
# Usage:
# curl -fsSL <url>/install.sh | sudo bash
#
# Options:
# --domain <domain> Your domain (required)
# --email <email> Admin email / Let's Encrypt
# --no-ssl Skip SSL (local testing only)
# --single-user Single user mode
# --s3 Enable S3 storage configuration
# =============================================================================
set -o pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
# Configuration
INSTALL_DIR="/opt/mastodon"
DATA_DIR="/opt/mastodon-data"
DOMAIN=""
ADMIN_EMAIL=""
ENABLE_SSL=true
SINGLE_USER_MODE=false
ENABLE_S3=false
# Parse arguments
while [ $# -gt 0 ]; do
case $1 in
--domain) DOMAIN="$2"; shift 2 ;;
--email) ADMIN_EMAIL="$2"; shift 2 ;;
--no-ssl) ENABLE_SSL=false; shift ;;
--single-user) SINGLE_USER_MODE=true; shift ;;
--s3) ENABLE_S3=true; shift ;;
--help|-h)
echo "Mastodon Production Installer"
echo ""
echo "Usage: install.sh [options]"
echo ""
echo "Options:"
echo " --domain <domain> Your domain (e.g., mastodon.example.com)"
echo " --email <email> Admin email for Let's Encrypt"
echo " --no-ssl Skip SSL (testing only)"
echo " --single-user Single user mode"
echo " --s3 Configure S3 storage"
exit 0
;;
*) shift ;;
esac
done
# Check root
[ "$(id -u)" -ne 0 ] && error "Run as root: sudo bash install.sh"
# Detect OS
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
OS_VERSION=${VERSION_ID:-}
else
error "Cannot detect OS"
fi
log "Detected: $OS $OS_VERSION"
}
# Wait for package manager locks
wait_for_lock() {
case $OS in
ubuntu|debian|linuxmint|pop)
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
sleep 2
done
;;
esac
}
# Install Docker
install_docker() {
if command -v docker >/dev/null 2>&1; then
success "Docker already installed"
systemctl enable --now docker 2>/dev/null || true
return
fi
log "Installing Docker..."
case $OS in
ubuntu|debian|linuxmint|pop)
export DEBIAN_FRONTEND=noninteractive
wait_for_lock
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
DOCKER_OS=$OS
case "$OS" in linuxmint|pop) DOCKER_OS="ubuntu" ;; esac
curl -fsSL https://download.docker.com/linux/$DOCKER_OS/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
chmod a+r /etc/apt/keyrings/docker.gpg
CODENAME=${VERSION_CODENAME:-jammy}
case "$OS" in linuxmint|pop) CODENAME="jammy" ;; esac
[ "$OS" = "debian" ] && case "$CODENAME" in trixie|sid) CODENAME="bookworm" ;; esac
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$DOCKER_OS $CODENAME stable" > /etc/apt/sources.list.d/docker.list
wait_for_lock
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
;;
fedora)
dnf install -y -q dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
dnf install -y -q docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
;;
rocky|almalinux|rhel|centos)
dnf install -y -q dnf-plugins-core || yum install -y yum-utils
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 2>/dev/null || \
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y -q docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || \
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
;;
arch|manjaro|endeavouros)
pacman -Sy --noconfirm docker docker-compose
;;
opensuse*|sles)
zypper install -y docker docker-compose
;;
*)
error "Unsupported OS: $OS"
;;
esac
systemctl enable --now docker
success "Docker installed"
}
# Generate secrets
generate_secrets() {
SECRET_KEY_BASE=$(openssl rand -hex 64)
OTP_SECRET=$(openssl rand -hex 64)
# Generate VAPID keys
VAPID_KEYS=$(docker run --rm tootsuite/mastodon:latest bundle exec rake mastodon:webpush:generate_vapid_key 2>/dev/null || echo "")
if [ -n "$VAPID_KEYS" ]; then
VAPID_PRIVATE_KEY=$(echo "$VAPID_KEYS" | grep VAPID_PRIVATE_KEY | cut -d= -f2)
VAPID_PUBLIC_KEY=$(echo "$VAPID_KEYS" | grep VAPID_PUBLIC_KEY | cut -d= -f2)
else
VAPID_PRIVATE_KEY=$(openssl rand -hex 32)
VAPID_PUBLIC_KEY=$(openssl rand -hex 32)
fi
POSTGRES_PASSWORD="REDACTED_PASSWORD" rand -hex 32)
REDIS_PASSWORD="REDACTED_PASSWORD" rand -hex 32)
}
# Get domain interactively
get_domain() {
if [ -z "$DOMAIN" ]; then
echo ""
echo "========================================"
echo " Domain Configuration"
echo "========================================"
echo ""
echo "Enter your domain for Mastodon (e.g., mastodon.example.com)"
echo "A domain is REQUIRED for Mastodon to work properly."
echo ""
read -p "Domain: " DOMAIN
if [ -z "$DOMAIN" ]; then
error "Domain is required for Mastodon"
fi
fi
if [ -z "$ADMIN_EMAIL" ]; then
read -p "Admin email: " ADMIN_EMAIL
if [ -z "$ADMIN_EMAIL" ]; then
warn "No email provided - SSL may not work"
ADMIN_EMAIL="admin@$DOMAIN"
fi
fi
}
# Create directories
create_directories() {
log "Creating directories..."
mkdir -p "$INSTALL_DIR"
mkdir -p "$DATA_DIR"/{postgres,redis,mastodon/{public/system,live}}
mkdir -p "$DATA_DIR"/caddy/{data,config}
chmod -R 755 "$DATA_DIR"
success "Directories created"
}
# Create .env file
create_env() {
log "Creating environment configuration..."
local protocol="https"
[ "$ENABLE_SSL" != true ] && protocol="http"
cat > "$INSTALL_DIR/.env.production" << EOF
# Federation
LOCAL_DOMAIN=$DOMAIN
SINGLE_USER_MODE=$SINGLE_USER_MODE
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD="REDACTED_PASSWORD"
# PostgreSQL
DB_HOST=db
DB_USER=mastodon
DB_NAME=mastodon
DB_PASS="REDACTED_PASSWORD"
DB_PORT=5432
# Secrets
SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET
VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
# Web
WEB_DOMAIN=$DOMAIN
ALTERNATE_DOMAINS=
# Email (configure for production)
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
"REDACTED_PASSWORD"
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
# File storage
# For S3 storage, uncomment and configure:
# S3_ENABLED=true
# S3_BUCKET=your-bucket
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=us-east-1
# S3_PROTOCOL=https
# S3_HOSTNAME=s3.amazonaws.com
# Elasticsearch (optional, for full-text search)
# ES_ENABLED=true
# ES_HOST=elasticsearch
# ES_PORT=9200
# Performance
RAILS_ENV=production
NODE_ENV=production
RAILS_LOG_LEVEL=warn
TRUSTED_PROXY_IP=172.16.0.0/12
# IP and session
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
EOF
chmod 600 "$INSTALL_DIR/.env.production"
success "Environment configuration created"
}
# Create docker-compose.yml
create_compose() {
log "Creating Docker Compose file..."
cat > "$INSTALL_DIR/docker-compose.yml" << 'EOF'
services:
db:
image: postgres:16-alpine
container_name: mastodon-db
shm_size: 256mb
environment:
POSTGRES_USER: mastodon
POSTGRES_PASSWORD: "REDACTED_PASSWORD"
POSTGRES_DB: mastodon
volumes:
- ./data/postgres:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD", "pg_isready", "-U", "mastodon"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
redis:
image: redis:7-alpine
container_name: mastodon-redis
command: redis-server --requirepass REDACTED_PASSWORD
volumes:
- ./data/redis:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:"REDACTED_PASSWORD" "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
web:
image: tootsuite/mastodon:latest
container_name: mastodon-web
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
volumes:
- ./data/mastodon/public/system:/mastodon/public/system
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
networks:
- internal
- external
streaming:
image: tootsuite/mastodon:latest
container_name: mastodon-streaming
env_file: .env.production
command: node ./streaming
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
networks:
- internal
- external
sidekiq:
image: tootsuite/mastodon:latest
container_name: mastodon-sidekiq
env_file: .env.production
command: bundle exec sidekiq
volumes:
- ./data/mastodon/public/system:/mastodon/public/system
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "ps aux | grep '[s]idekiq 6' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
networks:
- internal
- external
caddy:
image: caddy:2-alpine
container_name: mastodon-caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data/caddy/data:/data
- ./data/caddy/config:/config
- ./data/mastodon/public:/mastodon/public:ro
depends_on:
- web
- streaming
restart: unless-stopped
networks:
- external
watchtower:
image: containrrr/watchtower:latest
container_name: mastodon-watchtower
environment:
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_SCHEDULE: "0 0 4 * * *"
WATCHTOWER_LABEL_ENABLE: "false"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
networks:
internal:
internal: true
external:
EOF
# Extract DB_PASS for compose
echo "DB_PASS="REDACTED_PASSWORD" > "$INSTALL_DIR/.env"
echo "REDIS_PASSWORD="REDACTED_PASSWORD" >> "$INSTALL_DIR/.env"
success "Docker Compose file created"
}
# Create Caddyfile
create_caddyfile() {
log "Creating Caddy configuration..."
if [ "$ENABLE_SSL" = true ]; then
cat > "$INSTALL_DIR/Caddyfile" << EOF
$DOMAIN {
encode gzip
handle_path /system/* {
file_server {
root /mastodon/public
}
}
handle /api/v1/streaming/* {
reverse_proxy streaming:4000
}
handle /* {
reverse_proxy web:3000
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output stdout
}
}
EOF
else
cat > "$INSTALL_DIR/Caddyfile" << EOF
:80 {
encode gzip
handle_path /system/* {
file_server {
root /mastodon/public
}
}
handle /api/v1/streaming/* {
reverse_proxy streaming:4000
}
handle /* {
reverse_proxy web:3000
}
}
EOF
fi
success "Caddy configuration created"
}
# Initialize database
init_database() {
log "Initializing database..."
cd "$INSTALL_DIR"
# Start database first
docker compose up -d db redis
sleep 10
# Run migrations
docker compose run --rm web bundle exec rails db:setup SAFETY_ASSURED=1 2>/dev/null || \
docker compose run --rm web bundle exec rails db:migrate SAFETY_ASSURED=1
# Precompile assets
docker compose run --rm web bundle exec rails assets:precompile
success "Database initialized"
}
# Create management script
create_management_script() {
log "Creating management script..."
cat > /usr/local/bin/mastodon << 'EOF'
#!/bin/bash
cd /opt/mastodon || exit 1
case "${1:-help}" in
start) docker compose up -d ;;
stop) docker compose down ;;
restart) docker compose restart ${2:-} ;;
status) docker compose ps ;;
logs) docker compose logs -f ${2:-} ;;
update)
docker compose pull
docker compose up -d
docker compose run --rm web bundle exec rails db:migrate
docker compose run --rm web bundle exec rails assets:precompile
docker compose restart
;;
edit) ${EDITOR:-nano} /opt/mastodon/.env.production ;;
admin)
if [ -z "$2" ]; then
echo "Usage: mastodon admin <username>"
exit 1
fi
docker compose run --rm web bin/tootctl accounts create "$2" --email "${3:-admin@localhost}" --confirmed --role Owner
;;
reset-password)
if [ -z "$2" ]; then
echo "Usage: mastodon reset-password <username>"
exit 1
fi
docker compose run --rm web bin/tootctl accounts modify "$2" --reset-password
;;
tootctl)
shift
docker compose run --rm web bin/tootctl "$@"
;;
console)
docker compose run --rm web bin/rails console
;;
shell)
docker compose run --rm web /bin/bash
;;
backup)
timestamp=$(date +"%Y%m%d_%H%M%S")
backup_dir="/opt/mastodon-data/backups"
mkdir -p "$backup_dir"
echo "Backing up database..."
docker compose exec -T db pg_dump -U mastodon mastodon > "$backup_dir/mastodon_db_$timestamp.sql"
echo "Backing up media..."
tar -czf "$backup_dir/mastodon_media_$timestamp.tar.gz" -C /opt/mastodon-data mastodon/public/system
echo "Backup complete: $backup_dir"
ls -la "$backup_dir"/*$timestamp*
;;
cleanup)
echo "Cleaning up old media..."
docker compose run --rm web bin/tootctl media remove --days=7
docker compose run --rm web bin/tootctl preview_cards remove --days=30
docker compose run --rm web bin/tootctl statuses remove --days=90
;;
*)
echo "Mastodon Management"
echo ""
echo "Usage: mastodon <command>"
echo ""
echo "Commands:"
echo " start Start all services"
echo " stop Stop all services"
echo " restart [service] Restart services"
echo " status Show status"
echo " logs [service] View logs"
echo " update Update and migrate"
echo " edit Edit configuration"
echo " admin <user> Create admin user"
echo " reset-password <u> Reset user password"
echo " tootctl <args> Run tootctl command"
echo " console Rails console"
echo " shell Bash shell"
echo " backup Backup database and media"
echo " cleanup Clean old media/statuses"
;;
esac
EOF
chmod +x /usr/local/bin/mastodon
success "Management script created"
}
# Configure firewall
configure_firewall() {
log "Configuring firewall..."
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
firewall-cmd --permanent --add-service=http 2>/dev/null || true
firewall-cmd --permanent --add-service=https 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
success "Firewall configured (firewalld)"
elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q "active"; then
ufw allow 80/tcp 2>/dev/null || true
ufw allow 443/tcp 2>/dev/null || true
success "Firewall configured (ufw)"
else
warn "No active firewall detected"
fi
}
# Deploy
deploy() {
log "Deploying Mastodon..."
cd "$INSTALL_DIR"
# Copy data directory reference
ln -sf "$DATA_DIR" "$INSTALL_DIR/data" 2>/dev/null || true
mkdir -p "$INSTALL_DIR/data"
ln -sf "$DATA_DIR/postgres" "$INSTALL_DIR/data/postgres"
ln -sf "$DATA_DIR/redis" "$INSTALL_DIR/data/redis"
ln -sf "$DATA_DIR/mastodon" "$INSTALL_DIR/data/mastodon"
ln -sf "$DATA_DIR/caddy" "$INSTALL_DIR/data/caddy"
docker compose pull
# Initialize database
init_database
# Start all services
docker compose up -d
# Wait for services
log "Waiting for services to start..."
sleep 15
success "Mastodon deployed!"
}
# Show completion message
show_complete() {
local protocol="https"
[ "$ENABLE_SSL" != true ] && protocol="http"
echo ""
echo "========================================"
echo " Mastodon Installation Complete!"
echo "========================================"
echo ""
echo "Access:"
echo " Web Interface: ${protocol}://${DOMAIN}"
echo ""
echo "Create your admin account:"
echo " mastodon admin yourusername your@email.com"
echo ""
echo "Then reset password to get initial password:"
echo " mastodon reset-password yourusername"
echo ""
echo "Commands:"
echo " mastodon status - Show service status"
echo " mastodon logs - View logs"
echo " mastodon update - Update Mastodon"
echo " mastodon backup - Backup database"
echo " mastodon cleanup - Clean old media"
echo " mastodon tootctl - Run tootctl commands"
echo ""
echo "Config: $INSTALL_DIR/.env.production"
echo "Data: $DATA_DIR"
echo ""
echo "⚠️ Configure email in .env.production for:"
echo " - Email notifications"
echo " - Password resets"
echo " - Account confirmations"
echo ""
}
# Main
main() {
echo ""
echo "========================================"
echo " Mastodon Production Installer"
echo "========================================"
echo ""
detect_os
get_domain
generate_secrets
install_docker
create_directories
create_env
create_compose
create_caddyfile
create_management_script
configure_firewall
deploy
show_complete
}
main "$@"