Sanitized mirror from private repository - 2026-03-21 11:39:16 UTC
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m0s
Documentation / Deploy to GitHub Pages (push) Has been skipped

This commit is contained in:
Gitea Mirror Bot
2026-03-21 11:39:16 +00:00
commit fb4cb4fc23
1242 changed files with 308458 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Vish
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,160 @@
# Mastodon Production Scripts
Production-ready Mastodon deployment scripts for self-hosting.
## Installation Options
### Option 1: Docker (Multi-Platform)
```bash
curl -fsSL https://git.vish.gg/Vish/mastodon-production/raw/branch/main/install.sh | sudo bash -s -- --domain mastodon.example.com --email admin@example.com
```
Supports: Ubuntu, Debian, Fedora, Rocky/Alma/RHEL 8+, Arch, openSUSE
### Option 2: Bare-Metal (Rocky Linux 10)
```bash
# Set your configuration
export DOMAIN="mastodon.example.com"
export ADMIN_USER="admin"
export ADMIN_EMAIL="admin@example.com"
export SMTP_SERVER="smtp.gmail.com"
export SMTP_PORT="587"
export SMTP_USER="your@gmail.com"
export SMTP_PASS="REDACTED_PASSWORD"
export SMTP_FROM="notifications@example.com"
# Run installer
curl -sSL https://git.vish.gg/Vish/mastodon-production/raw/branch/main/install-baremetal.sh | bash
```
## Scripts
| Script | Description |
|--------|-------------|
| `install.sh` | Docker-based installer (multi-platform) |
| `install-baremetal.sh` | Bare-metal installer for Rocky Linux 10 |
| `verify-mastodon.sh` | Health check / verification script |
| `fix-mastodon.sh` | Diagnose and auto-fix common issues |
| `backup-mastodon.sh` | Backup script for migration |
| `update-mastodon.sh` | Update to latest Mastodon version |
### Verify Installation
```bash
./verify-mastodon.sh
```
Checks:
- All services (postgresql, valkey, nginx, mastodon-*)
- API endpoints (instance, streaming)
- Database connectivity and stats
- Federation endpoints (webfinger, nodeinfo)
- Configuration files
### Fix Common Issues
```bash
./fix-mastodon.sh
```
Automatically fixes:
- Stopped services
- File permissions
- SELinux contexts
- Service startup issues
## Bare-Metal Architecture (Rocky Linux 10)
```
Internet → Cloudflare → Reverse Proxy (443) → Rocky VM (3000)
nginx
┌─────────────────┼─────────────────┐
↓ ↓ ↓
Puma (3001) Streaming (4000) Sidekiq
↓ ↓ ↓
└─────────────────┼─────────────────┘
PostgreSQL + Valkey
```
### Services (Bare-Metal)
| Service | Port | Description |
|---------|------|-------------|
| nginx | 3000 | External reverse proxy |
| mastodon-web | 3001 | Puma web server |
| mastodon-streaming | 4000 | WebSocket streaming |
| mastodon-sidekiq | - | Background jobs |
| postgresql | 5432 | Database |
| valkey | 6379 | Redis cache |
## Backup & Restore
### Create Backup
```bash
/home/mastodon/scripts/backup-mastodon.sh
```
Creates a complete backup including:
- PostgreSQL database dump
- `.env.production` (secrets)
- User uploads (avatars, headers, media)
- Restore instructions
### Restore
See `RESTORE.md` included in backup archive.
## Update Mastodon
```bash
# Update to latest version
/home/mastodon/scripts/update-mastodon.sh
# Update to specific version
/home/mastodon/scripts/update-mastodon.sh v4.6.0
```
## Maintenance Commands
```bash
# Service status
systemctl status mastodon-web mastodon-sidekiq mastodon-streaming
# Restart all services
systemctl restart mastodon-web mastodon-sidekiq mastodon-streaming
# View logs
journalctl -u mastodon-web -f
journalctl -u mastodon-sidekiq -f
# Access tootctl
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl --help'
# Create new user
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts create USERNAME --email=EMAIL --confirmed'
# Make user admin/owner
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --role Owner'
# Clear media cache
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl media remove --days=7'
```
## Requirements
### Bare-Metal
- Rocky Linux 10 (fresh install)
- 4GB+ RAM recommended
- 20GB+ disk space
- Domain with DNS configured
- SMTP credentials for email
### Docker
- Any supported Linux distribution
- Docker and Docker Compose
- Domain with DNS configured
## License
MIT

View File

@@ -0,0 +1,140 @@
# User Management Guide
## Creating New Users
### Method 1: Command Line (Recommended for Admins)
```bash
# Create a new user (confirmed = skip email verification)
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts create USERNAME --email=user@example.com --confirmed'
# Approve the user (if approval mode is enabled)
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts approve USERNAME'
# Optional: Give them a role
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --role Moderator'
# Roles: Owner, Admin, Moderator (or leave blank for regular user)
```
### Method 2: Web Registration
1. Go to https://your-domain.com
2. Click "Create account"
3. Fill in username, email, password
4. Admin approves in Settings → Administration → Pending accounts (if approval required)
### Method 3: Invite Links
1. Login as admin
2. Go to Settings → Invites
3. Click "Generate invite link"
4. Share the link with your partner/friends
## Example: Adding Your Partner
```bash
# Create account for partner
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts create partner --email=partner@example.com --confirmed'
# Save the generated password! It will be displayed like:
# New password: "REDACTED_PASSWORD"
# Approve the account
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts approve partner'
# Optional: Make them an admin too
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify partner --role Admin'
```
## User Limits
**There is NO hard limit on users.**
Your only constraints are server resources:
- **RAM**: Each active user session uses some memory
- **Storage**: Media uploads (avatars, images, videos) take disk space
- **CPU**: More users = more background jobs
For a small personal instance (2-10 users), a VM with 4GB RAM and 20GB storage is more than enough.
## Managing Existing Users
### List all users
```bash
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts list'
```
### Reset a user's password
```bash
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --reset-password'
```
### Disable/Enable a user
```bash
# Disable
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --disable'
# Enable
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --enable'
```
### Delete a user
```bash
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts delete USERNAME'
```
### Change user role
```bash
# Make admin
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --role Admin'
# Make moderator
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --role Moderator'
# Remove all roles (regular user)
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/tootctl accounts modify USERNAME --role ""'
```
## Registration Settings
Control how new users can join via the admin panel:
1. Login as admin
2. Go to **Settings → Administration → Server Settings → Registrations**
3. Choose:
- **Open**: Anyone can sign up
- **Approval required**: Admin must approve new accounts
- **Closed**: No new registrations (invite-only)
## User Roles
| Role | Permissions |
|------|-------------|
| **Owner** | Full access, can't be demoted |
| **Admin** | Full admin panel access, manage users, server settings |
| **Moderator** | Handle reports, suspend users, manage content |
| **User** | Regular user, no admin access |
## Quick Reference
```bash
# Create user
bin/tootctl accounts create USERNAME --email=EMAIL --confirmed
# Approve user
bin/tootctl accounts approve USERNAME
# Make admin
bin/tootctl accounts modify USERNAME --role Admin
# Reset password
bin/tootctl accounts modify USERNAME --reset-password
# Delete user
bin/tootctl accounts delete USERNAME
```
All commands require the prefix:
```bash
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production ...'
```

View File

@@ -0,0 +1,131 @@
#!/bin/bash
# Mastodon Backup Script
# Creates a complete backup for migration to another server
# Run as root
set -e
BACKUP_DIR="${BACKUP_DIR:-/home/mastodon/backups}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="mastodon_backup_${TIMESTAMP}"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"
echo "=========================================="
echo "Mastodon Backup Script"
echo "Backup location: ${BACKUP_PATH}"
echo "=========================================="
# Create backup directory
mkdir -p "${BACKUP_PATH}"
# 1. Backup PostgreSQL database
echo "[1/5] Backing up PostgreSQL database..."
sudo -u postgres pg_dump -Fc mastodon_production > "${BACKUP_PATH}/database.dump"
echo " Database backup: $(du -h ${BACKUP_PATH}/database.dump | cut -f1)"
# 2. Backup .env.production (contains secrets)
echo "[2/5] Backing up configuration..."
cp /home/mastodon/live/.env.production "${BACKUP_PATH}/.env.production"
# 3. Backup user uploads (avatars, headers, media)
echo "[3/5] Backing up user uploads (this may take a while)..."
if [ -d /home/mastodon/live/public/system ]; then
tar -czf "${BACKUP_PATH}/system.tar.gz" -C /home/mastodon/live/public system
echo " System files: $(du -h ${BACKUP_PATH}/system.tar.gz | cut -f1)"
else
echo " No system directory found (fresh install)"
fi
# 4. Backup custom files (if any)
echo "[4/5] Backing up custom files..."
mkdir -p "${BACKUP_PATH}/custom"
# Custom CSS/branding
if [ -f /home/mastodon/live/app/javascript/styles/custom.scss ]; then
cp /home/mastodon/live/app/javascript/styles/custom.scss "${BACKUP_PATH}/custom/"
fi
# Site uploads (favicon, thumbnail, etc)
if [ -d /home/mastodon/live/public/site_uploads ]; then
cp -r /home/mastodon/live/public/site_uploads "${BACKUP_PATH}/custom/"
fi
# 5. Export user data
echo "[5/5] Exporting instance data..."
sudo -u mastodon bash -c "cd ~/live && export PATH=\"\$HOME/.rbenv/bin:\$PATH\" && eval \"\$(rbenv init -)\" && RAILS_ENV=production bin/tootctl accounts export > /dev/null 2>&1" || true
# Create restore instructions
cat > "${BACKUP_PATH}/RESTORE.md" << 'RESTORE'
# Mastodon Restore Instructions
## On the new server:
1. Run the install script first (without creating admin user)
2. Stop all Mastodon services:
```
systemctl stop mastodon-web mastodon-sidekiq mastodon-streaming
```
3. Restore the database:
```
sudo -u postgres dropdb mastodon_production
sudo -u postgres createdb -O mastodon mastodon_production
sudo -u postgres pg_restore -d mastodon_production database.dump
```
4. Restore .env.production:
```
cp .env.production /home/mastodon/live/.env.production
chown mastodon:mastodon /home/mastodon/live/.env.production
chmod 600 /home/mastodon/live/.env.production
```
5. Restore user uploads:
```
cd /home/mastodon/live/public
tar -xzf /path/to/backup/system.tar.gz
chown -R mastodon:mastodon system
```
6. Update LOCAL_DOMAIN in .env.production if domain changed
7. Run migrations (in case of version upgrade):
```
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bundle exec rails db:migrate'
```
8. Recompile assets:
```
sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bundle exec rails assets:precompile'
```
9. Fix SELinux contexts:
```
chcon -R -t httpd_sys_content_t /home/mastodon/live/public
```
10. Start services:
```
systemctl start mastodon-web mastodon-sidekiq mastodon-streaming
```
RESTORE
# Create final archive
echo ""
echo "Creating final archive..."
cd "${BACKUP_DIR}"
tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}"
rm -rf "${BACKUP_NAME}"
FINAL_SIZE=$(du -h "${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" | cut -f1)
echo ""
echo "=========================================="
echo "✅ Backup Complete!"
echo "=========================================="
echo ""
echo "Backup file: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
echo "Size: ${FINAL_SIZE}"
echo ""
echo "To download: scp root@server:${BACKUP_DIR}/${BACKUP_NAME}.tar.gz ."
echo ""

View File

@@ -0,0 +1,222 @@
#!/bin/bash
# =============================================================================
# Mastodon Fix/Repair Script
# Diagnoses and fixes common issues
# =============================================================================
# Run as root
echo "=========================================="
echo "Mastodon Fix/Repair Tool"
echo "=========================================="
# Check root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
FIXED=0
ERRORS=0
# 1. Check and fix service status
echo ""
echo "[1/7] Checking services..."
services=("postgresql" "valkey" "nginx" "mastodon-web" "mastodon-sidekiq" "mastodon-streaming")
for svc in "${services[@]}"; do
if systemctl is-active --quiet $svc 2>/dev/null; then
echo "$svc is running"
elif systemctl list-unit-files | grep -q "^${svc}.service"; then
echo "$svc is not running, attempting to start..."
systemctl start $svc 2>/dev/null
sleep 2
if systemctl is-active --quiet $svc; then
echo "$svc started successfully"
FIXED=$((FIXED + 1))
else
echo " ✗ Failed to start $svc"
echo " Check logs: journalctl -u $svc -n 50"
ERRORS=$((ERRORS + 1))
fi
fi
done
# 2. Check file permissions
echo ""
echo "[2/7] Checking file permissions..."
# Check .env.production
if [ -f /home/mastodon/live/.env.production ]; then
OWNER=$(stat -c '%U' /home/mastodon/live/.env.production)
PERMS=$(stat -c '%a' /home/mastodon/live/.env.production)
if [ "$OWNER" != "mastodon" ]; then
echo " ✗ Fixing .env.production ownership..."
chown mastodon:mastodon /home/mastodon/live/.env.production
FIXED=$((FIXED + 1))
fi
if [ "$PERMS" != "600" ]; then
echo " ✗ Fixing .env.production permissions..."
chmod 600 /home/mastodon/live/.env.production
FIXED=$((FIXED + 1))
fi
echo " ✓ .env.production permissions OK"
fi
# Check live directory ownership
if [ -d /home/mastodon/live ]; then
LIVE_OWNER=$(stat -c '%U' /home/mastodon/live)
if [ "$LIVE_OWNER" != "mastodon" ]; then
echo " ✗ Fixing /home/mastodon/live ownership..."
chown -R mastodon:mastodon /home/mastodon/live
FIXED=$((FIXED + 1))
else
echo " ✓ /home/mastodon/live ownership OK"
fi
fi
# 3. Check database connection
echo ""
echo "[3/7] Checking database..."
if sudo -u postgres psql -c "SELECT 1" mastodon_production > /dev/null 2>&1; then
echo " ✓ Database connection successful"
else
echo " ✗ Cannot connect to database"
# Try to fix common issues
if ! systemctl is-active --quiet postgresql; then
echo " Attempting to start PostgreSQL..."
systemctl start postgresql
sleep 2
fi
# Check if database exists
if ! sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw mastodon_production; then
echo " Database does not exist!"
ERRORS=$((ERRORS + 1))
fi
fi
# 4. Check Redis/Valkey connection
echo ""
echo "[4/7] Checking cache server..."
if valkey-cli ping > /dev/null 2>&1; then
echo " ✓ Valkey connection successful"
elif redis-cli ping > /dev/null 2>&1; then
echo " ✓ Redis connection successful"
else
echo " ✗ Cannot connect to cache server"
if systemctl is-active --quiet valkey; then
echo " Valkey is running but not responding"
elif systemctl is-active --quiet redis; then
echo " Redis is running but not responding"
else
echo " Attempting to start Valkey..."
systemctl start valkey 2>/dev/null || systemctl start redis 2>/dev/null
sleep 2
FIXED=$((FIXED + 1))
fi
fi
# 5. Check nginx configuration
echo ""
echo "[5/7] Checking nginx configuration..."
if nginx -t 2>/dev/null; then
echo " ✓ Nginx configuration is valid"
else
echo " ✗ Nginx configuration has errors"
nginx -t
ERRORS=$((ERRORS + 1))
fi
# 6. Check SELinux contexts (Rocky/RHEL)
echo ""
echo "[6/7] Checking SELinux..."
if command -v getenforce &> /dev/null; then
SELINUX_MODE=$(getenforce)
echo " SELinux mode: $SELINUX_MODE"
if [ "$SELINUX_MODE" = "Enforcing" ]; then
# Fix common SELinux issues
if [ -d /home/mastodon/live/public ]; then
echo " Ensuring correct SELinux contexts..."
chcon -R -t httpd_sys_content_t /home/mastodon/live/public 2>/dev/null || true
fi
fi
else
echo " SELinux not present"
fi
# 7. Check API endpoints
echo ""
echo "[7/7] Checking API endpoints..."
sleep 1
# Test instance API
if curl -sf http://127.0.0.1:3000/api/v1/instance > /dev/null 2>&1; then
echo " ✓ Instance API responding"
else
echo " ✗ Instance API not responding"
# Check if it's a startup timing issue
echo " Waiting for services to fully start..."
sleep 5
if curl -sf http://127.0.0.1:3000/api/v1/instance > /dev/null 2>&1; then
echo " ✓ Instance API now responding"
else
echo " ✗ Instance API still not responding"
echo " Check logs: journalctl -u mastodon-web -n 50"
ERRORS=$((ERRORS + 1))
fi
fi
# Test streaming API
if curl -sf http://127.0.0.1:4000/api/v1/streaming/health > /dev/null 2>&1; then
echo " ✓ Streaming API healthy"
else
echo " ✗ Streaming API not responding"
echo " Attempting to restart streaming service..."
systemctl restart mastodon-streaming
sleep 3
if curl -sf http://127.0.0.1:4000/api/v1/streaming/health > /dev/null 2>&1; then
echo " ✓ Streaming API now healthy"
FIXED=$((FIXED + 1))
else
echo " ✗ Streaming API still not responding"
ERRORS=$((ERRORS + 1))
fi
fi
# Summary
echo ""
echo "=========================================="
if [ $ERRORS -eq 0 ]; then
if [ $FIXED -eq 0 ]; then
echo "✅ All checks passed! No issues found."
else
echo "✅ Fixed $FIXED issue(s). All checks now pass."
echo ""
echo "You may want to restart services:"
echo " systemctl restart mastodon-web mastodon-sidekiq mastodon-streaming"
fi
else
echo "⚠️ Found $ERRORS error(s) that need manual attention."
echo ""
echo "Common fixes:"
echo " - Check logs: journalctl -u mastodon-web -f"
echo " - Restart all: systemctl restart mastodon-{web,sidekiq,streaming}"
echo " - Check .env: cat /home/mastodon/live/.env.production"
echo " - Run migrations: sudo -u mastodon bash -lc 'cd ~/live && RAILS_ENV=production bin/rails db:migrate'"
fi
echo "=========================================="
exit $ERRORS

View File

@@ -0,0 +1,340 @@
#!/bin/bash
# Mastodon v4.5.4 Bare-Metal Install Script for Rocky Linux 10
# Usage: curl -sSL https://git.vish.gg/Vish/pihole-baremetal/raw/branch/main/mastodon/install-mastodon.sh | bash
# Run as root on a fresh Rocky Linux 10 VM
set -e
# Configuration - Edit these before running
DOMAIN="${DOMAIN:-mastodon.example.com}"
ADMIN_USER="${ADMIN_USER:-admin}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}"
SMTP_SERVER="${SMTP_SERVER:-smtp.gmail.com}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_USER="${SMTP_USER:-}"
SMTP_PASS="REDACTED_PASSWORD"
SMTP_FROM="${SMTP_FROM:-notifications@example.com}"
echo "=========================================="
echo "Mastodon v4.5.4 Installation Script"
echo "Target Domain: $DOMAIN"
echo "=========================================="
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
# Install system dependencies
echo "[1/12] Installing system dependencies..."
dnf install -y epel-release
dnf install -y git curl wget gcc make autoconf bison openssl-devel \
libyaml-devel libffi-devel readline-devel zlib-devel gdbm-devel ncurses-devel \
libxml2-devel libxslt-devel libicu-devel libidn-devel jemalloc-devel \
ImageMagick ImageMagick-devel nginx postgresql-server postgresql-contrib \
valkey certbot python3-certbot-nginx meson ninja-build \
libpng-devel libjpeg-turbo-devel libwebp-devel libtiff-devel \
expat-devel gobject-introspection-devel glib2-devel
# Install Node.js 20
echo "[2/12] Installing Node.js 20..."
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
dnf install -y nodejs
# Enable corepack for Yarn
corepack enable
# Build libvips from source (not in Rocky 10 repos)
echo "[3/12] Building libvips from source..."
cd /tmp
wget https://github.com/libvips/libvips/releases/download/v8.16.1/vips-8.16.1.tar.xz
tar xf vips-8.16.1.tar.xz
cd vips-8.16.1
meson setup build --prefix=/usr --buildtype=release
cd build && ninja && ninja install
ldconfig
cd /tmp && rm -rf vips-8.16.1*
# Initialize PostgreSQL
echo "[4/12] Setting up PostgreSQL..."
postgresql-setup --initdb
systemctl enable --now postgresql
# Create mastodon database user and database
sudo -u postgres psql -c "CREATE USER mastodon CREATEDB;"
sudo -u postgres psql -c "CREATE DATABASE mastodon_production OWNER mastodon;"
# Start Valkey (Redis)
echo "[5/12] Starting Valkey..."
systemctl enable --now valkey
# Create mastodon user
echo "[6/12] Creating mastodon user..."
useradd -m -s /bin/bash mastodon || true
# Install Ruby via rbenv
echo "[7/12] Installing Ruby 3.4.7..."
sudo -u mastodon bash << 'RUBY_INSTALL'
cd ~
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
RUBY_CONFIGURE_OPTS="--with-jemalloc" rbenv install 3.4.7
rbenv global 3.4.7
gem install bundler
RUBY_INSTALL
# Clone Mastodon
echo "[8/12] Cloning Mastodon v4.5.4..."
sudo -u mastodon bash << 'CLONE'
cd ~
git clone https://github.com/mastodon/mastodon.git live
cd live
git checkout v4.5.4
CLONE
# Install dependencies
echo "[9/12] Installing Ruby and Node dependencies..."
sudo -u mastodon bash << 'DEPS'
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
cd ~/live
bundle config deployment 'true'
bundle config without 'development test'
bundle install -j$(nproc)
yarn install --immutable
DEPS
# Generate secrets and create .env.production
echo "[10/12] Generating secrets and configuration..."
SECRET_KEY=$(openssl rand -hex 64)
OTP_SECRET=$(openssl rand -hex 64)
VAPID_KEYS=$(sudo -u mastodon bash -c 'cd ~/live && export PATH="$HOME/.rbenv/bin:$PATH" && eval "$(rbenv init -)" && RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key 2>/dev/null')
VAPID_PRIVATE=$(echo "$VAPID_KEYS" | grep VAPID_PRIVATE_KEY | cut -d= -f2)
VAPID_PUBLIC=$(echo "$VAPID_KEYS" | grep VAPID_PUBLIC_KEY | cut -d= -f2)
AR_KEY=$(openssl rand -hex 32)
AR_DETERMINISTIC=$(openssl rand -hex 32)
AR_SALT=$(openssl rand -hex 32)
cat > /home/mastodon/live/.env.production << ENVFILE
LOCAL_DOMAIN=$DOMAIN
SINGLE_USER_MODE=false
SECRET_KEY_BASE=$SECRET_KEY
OTP_SECRET=$OTP_SECRET
VAPID_PRIVATE_KEY=$VAPID_PRIVATE
VAPID_PUBLIC_KEY=$VAPID_PUBLIC
DB_HOST=/var/run/postgresql
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=
"REDACTED_PASSWORD"
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
SMTP_SERVER=$SMTP_SERVER
SMTP_PORT=$SMTP_PORT
SMTP_LOGIN=$SMTP_USER
SMTP_PASSWORD="REDACTED_PASSWORD"
SMTP_FROM_ADDRESS=$SMTP_FROM
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$AR_KEY
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$AR_DETERMINISTIC
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$AR_SALT
TRUSTED_PROXY_IP=127.0.0.1,::1,192.168.0.0/16
ENVFILE
chown mastodon:mastodon /home/mastodon/live/.env.production
chmod 600 /home/mastodon/live/.env.production
# Run migrations and seed
echo "[11/12] Running database migrations..."
sudo -u mastodon bash << 'MIGRATE'
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
cd ~/live
RAILS_ENV=production bundle exec rails db:migrate
RAILS_ENV=production bundle exec rails db:seed
RAILS_ENV=production bundle exec rails assets:precompile
MIGRATE
# Create systemd services
echo "[12/12] Creating systemd services..."
cat > /etc/systemd/system/mastodon-web.service << 'SERVICE'
[Unit]
Description=mastodon-web
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3001"
ExecStart=/bin/bash -lc 'cd /home/mastodon/live && exec bundle exec puma -C config/puma.rb'
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE
cat > /etc/systemd/system/mastodon-sidekiq.service << 'SERVICE'
[Unit]
Description=mastodon-sidekiq
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="MALLOC_ARENA_MAX=2"
ExecStart=/bin/bash -lc 'cd /home/mastodon/live && exec bundle exec sidekiq -c 25'
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE
cat > /etc/systemd/system/mastodon-streaming.service << 'SERVICE'
[Unit]
Description=mastodon-streaming
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
Environment="STREAMING_CLUSTER_NUM=1"
ExecStart=/usr/bin/node ./streaming
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE
# Nginx config
cat > /etc/nginx/conf.d/mastodon.conf << 'NGINX'
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream backend {
server 127.0.0.1:3001 fail_timeout=0;
}
upstream streaming {
server 127.0.0.1:4000 fail_timeout=0;
}
server {
listen 3000;
listen [::]:3000;
server_name _;
keepalive_timeout 70;
sendfile on;
client_max_body_size 99m;
root /home/mastodon/live/public;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
location / {
try_files $uri @proxy;
}
location ~ ^/(assets|avatars|emoji|headers|packs|shortcuts|sounds|system)/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
try_files $uri =404;
}
location ^~ /api/v1/streaming {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://streaming;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
location @proxy {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://backend;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
error_page 404 500 501 502 503 504 /500.html;
}
NGINX
# SELinux and firewall
setsebool -P httpd_can_network_connect 1
setsebool -P httpd_read_user_content 1
chcon -R -t httpd_sys_content_t /home/mastodon/live/public
chmod 755 /home/mastodon /home/mastodon/live /home/mastodon/live/public
firewall-cmd --permanent --add-port=3000/tcp
firewall-cmd --reload
# Add localhost to Rails hosts
echo 'Rails.application.config.hosts << "localhost"' >> /home/mastodon/live/config/environments/production.rb
echo 'Rails.application.config.hosts << "127.0.0.1"' >> /home/mastodon/live/config/environments/production.rb
chown mastodon:mastodon /home/mastodon/live/config/environments/production.rb
# Enable and start services
systemctl daemon-reload
systemctl enable --now mastodon-web mastodon-sidekiq mastodon-streaming nginx
# Create admin user
echo ""
echo "Creating admin user..."
ADMIN_PASS="REDACTED_PASSWORD" -u mastodon bash -c "cd ~/live && export PATH=\"\$HOME/.rbenv/bin:\$PATH\" && eval \"\$(rbenv init -)\" && RAILS_ENV=production bin/tootctl accounts create $ADMIN_USER --email=$ADMIN_EMAIL --confirmed 2>&1 | grep 'New password' | awk '{print \$3}'")
sudo -u mastodon bash -c "cd ~/live && export PATH=\"\$HOME/.rbenv/bin:\$PATH\" && eval \"\$(rbenv init -)\" && RAILS_ENV=production bin/tootctl accounts modify $ADMIN_USER --role Owner"
sudo -u mastodon bash -c "cd ~/live && export PATH=\"\$HOME/.rbenv/bin:\$PATH\" && eval \"\$(rbenv init -)\" && RAILS_ENV=production bin/tootctl accounts approve $ADMIN_USER"
echo ""
echo "=========================================="
echo "✅ Mastodon Installation Complete!"
echo "=========================================="
echo ""
echo "Domain: $DOMAIN"
echo "Admin User: $ADMIN_USER"
echo "Admin Email: $ADMIN_EMAIL"
echo "Admin Password: "REDACTED_PASSWORD"
echo ""
echo "Listening on port 3000 (HTTP)"
echo ""
echo "Next steps:"
echo "1. Configure your reverse proxy to forward HTTPS to port 3000"
echo "2. Login and change your password"
echo "3. Configure instance settings in Administration panel"
echo ""

View File

@@ -0,0 +1,723 @@
#!/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 "$@"

View File

@@ -0,0 +1,105 @@
#!/bin/bash
# Mastodon Update Script
# Updates Mastodon to the latest stable version (or specified version)
# Run as root
set -e
TARGET_VERSION="${1:-}"
MASTODON_DIR="/home/mastodon/live"
echo "=========================================="
echo "Mastodon Update Script"
echo "=========================================="
# Check current version
CURRENT_VERSION=$(cd $MASTODON_DIR && git describe --tags 2>/dev/null || echo "unknown")
echo "Current version: $CURRENT_VERSION"
# Get latest version if not specified
if [ -z "$TARGET_VERSION" ]; then
echo "Fetching latest version..."
cd $MASTODON_DIR
sudo -u mastodon git fetch --tags
TARGET_VERSION=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
fi
echo "Target version: $TARGET_VERSION"
if [ "$CURRENT_VERSION" = "$TARGET_VERSION" ]; then
echo "Already at version $TARGET_VERSION. Nothing to do."
exit 0
fi
read -p "Proceed with update? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Update cancelled."
exit 1
fi
# Create backup first
echo ""
echo "[1/7] Creating backup before update..."
/home/mastodon/scripts/backup-mastodon.sh || echo "Backup script not found, skipping..."
# Stop services
echo ""
echo "[2/7] Stopping Mastodon services..."
systemctl stop mastodon-web mastodon-sidekiq mastodon-streaming
# Update code
echo ""
echo "[3/7] Updating Mastodon code..."
cd $MASTODON_DIR
sudo -u mastodon git fetch --all
sudo -u mastodon git checkout $TARGET_VERSION
# Update Ruby dependencies
echo ""
echo "[4/7] Updating Ruby dependencies..."
sudo -u mastodon bash -lc "cd ~/live && bundle install"
# Update Node dependencies
echo ""
echo "[5/7] Updating Node dependencies..."
sudo -u mastodon bash -lc "cd ~/live && yarn install --immutable"
# Run database migrations
echo ""
echo "[6/7] Running database migrations..."
sudo -u mastodon bash -lc "cd ~/live && RAILS_ENV=production bundle exec rails db:migrate"
# Precompile assets
echo ""
echo "[7/7] Precompiling assets (this may take a few minutes)..."
sudo -u mastodon bash -lc "cd ~/live && RAILS_ENV=production bundle exec rails assets:precompile"
# Fix SELinux contexts
chcon -R -t httpd_sys_content_t /home/mastodon/live/public
# Start services
echo ""
echo "Starting Mastodon services..."
systemctl start mastodon-web mastodon-sidekiq mastodon-streaming
# Verify
sleep 5
echo ""
echo "Checking service status..."
systemctl is-active mastodon-web mastodon-sidekiq mastodon-streaming
NEW_VERSION=$(cd $MASTODON_DIR && git describe --tags 2>/dev/null || echo "unknown")
echo ""
echo "=========================================="
echo "✅ Update Complete!"
echo "=========================================="
echo ""
echo "Previous version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
echo ""
echo "Please verify your instance is working correctly."
echo "Check the release notes for any manual steps:"
echo "https://github.com/mastodon/mastodon/releases/tag/$TARGET_VERSION"
echo ""

View File

@@ -0,0 +1,185 @@
#!/bin/bash
# =============================================================================
# Mastodon Health Check / Verification Script
# =============================================================================
# Run as root
echo "=========================================="
echo "Mastodon Health Check"
echo "=========================================="
echo ""
FAILED=0
WARN=0
# Load domain from .env if available
if [ -f /home/mastodon/live/.env.production ]; then
DOMAIN=$(grep "^LOCAL_DOMAIN=" /home/mastodon/live/.env.production | cut -d= -f2)
echo "Domain: ${DOMAIN:-unknown}"
fi
echo ""
echo "[Service Status]"
services=("postgresql" "valkey" "nginx" "mastodon-web" "mastodon-sidekiq" "mastodon-streaming")
for svc in "${services[@]}"; do
STATUS=$(systemctl is-active $svc 2>/dev/null || echo "not-found")
if [ "$STATUS" = "active" ]; then
echo "$svc: running"
elif [ "$STATUS" = "not-found" ]; then
echo " - $svc: not installed"
else
echo "$svc: $STATUS"
FAILED=1
fi
done
echo ""
echo "[API Endpoints]"
# Instance API
INSTANCE=$(curl -sf http://127.0.0.1:3000/api/v1/instance 2>/dev/null)
if [ -n "$INSTANCE" ]; then
VERSION=$(echo "$INSTANCE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','unknown'))" 2>/dev/null)
USERS=$(echo "$INSTANCE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('stats',{}).get('user_count',0))" 2>/dev/null)
echo " ✓ Instance API: responding (v$VERSION, $USERS users)"
else
echo " ✗ Instance API: not responding"
FAILED=1
fi
# Streaming API
STREAMING=$(curl -sf http://127.0.0.1:4000/api/v1/streaming/health 2>/dev/null)
if [ -n "$STREAMING" ]; then
echo " ✓ Streaming API: healthy"
else
echo " ✗ Streaming API: not responding"
FAILED=1
fi
# Nginx proxy
NGINX_CHECK=$(curl -sf -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/ 2>/dev/null)
if [ "$NGINX_CHECK" = "200" ] || [ "$NGINX_CHECK" = "302" ]; then
echo " ✓ Nginx proxy: working (HTTP $NGINX_CHECK)"
else
echo " ✗ Nginx proxy: not working (HTTP $NGINX_CHECK)"
FAILED=1
fi
echo ""
echo "[Database]"
if systemctl is-active --quiet postgresql; then
DB_SIZE=$(sudo -u postgres psql -t -c "SELECT pg_size_pretty(pg_database_size('mastodon_production'));" 2>/dev/null | xargs)
ACCOUNTS=$(sudo -u postgres psql -t -d mastodon_production -c "SELECT COUNT(*) FROM accounts;" 2>/dev/null | xargs)
STATUSES=$(sudo -u postgres psql -t -d mastodon_production -c "SELECT COUNT(*) FROM statuses;" 2>/dev/null | xargs)
echo " ✓ PostgreSQL: running (DB: ${DB_SIZE:-unknown})"
echo " Accounts: ${ACCOUNTS:-0}, Statuses: ${STATUSES:-0}"
else
echo " ✗ PostgreSQL: not running"
FAILED=1
fi
echo ""
echo "[Cache]"
if systemctl is-active --quiet valkey; then
VALKEY_INFO=$(valkey-cli INFO server 2>/dev/null | grep valkey_version | cut -d: -f2 | tr -d '\r')
echo " ✓ Valkey: running (v${VALKEY_INFO:-unknown})"
elif systemctl is-active --quiet redis; then
REDIS_INFO=$(redis-cli INFO server 2>/dev/null | grep redis_version | cut -d: -f2 | tr -d '\r')
echo " ✓ Redis: running (v${REDIS_INFO:-unknown})"
else
echo " ✗ Valkey/Redis: not running"
FAILED=1
fi
echo ""
echo "[Sidekiq Jobs]"
# Check sidekiq process
SIDEKIQ_PID=$(pgrep -f "sidekiq.*live" 2>/dev/null)
if [ -n "$SIDEKIQ_PID" ]; then
SIDEKIQ_MEM=$(ps -p $SIDEKIQ_PID -o rss= 2>/dev/null | awk '{printf "%.0fMB", $1/1024}')
echo " ✓ Sidekiq: running (PID: $SIDEKIQ_PID, Mem: $SIDEKIQ_MEM)"
else
echo " ✗ Sidekiq: not running"
FAILED=1
fi
echo ""
echo "[Federation]"
# Check webfinger
if [ -n "$DOMAIN" ]; then
WF_CHECK=$(curl -sf -H "Accept: application/jrd+json" "http://127.0.0.1:3000/.well-known/webfinger?resource=acct:test@$DOMAIN" 2>/dev/null | head -c 50)
if [ -n "$WF_CHECK" ]; then
echo " ✓ Webfinger: responding"
else
echo " - Webfinger: no test account (may be normal)"
fi
# Check host-meta
HOSTMETA=$(curl -sf "http://127.0.0.1:3000/.well-known/host-meta" 2>/dev/null | head -c 50)
if [ -n "$HOSTMETA" ]; then
echo " ✓ Host-meta: configured"
else
echo " ✗ Host-meta: not responding"
WARN=1
fi
# Check nodeinfo
NODEINFO=$(curl -sf "http://127.0.0.1:3000/nodeinfo/2.0" 2>/dev/null)
if [ -n "$NODEINFO" ]; then
echo " ✓ NodeInfo: available"
else
echo " ✗ NodeInfo: not responding"
WARN=1
fi
fi
echo ""
echo "[Storage]"
if [ -d /home/mastodon/live/public/system ]; then
MEDIA_SIZE=$(du -sh /home/mastodon/live/public/system 2>/dev/null | cut -f1)
echo " Media storage: ${MEDIA_SIZE:-empty}"
else
echo " Media storage: not yet created"
fi
DISK_USAGE=$(df -h /home 2>/dev/null | tail -1 | awk '{print $5}')
echo " Disk usage (/home): ${DISK_USAGE:-unknown}"
echo ""
echo "[Configuration]"
if [ -f /home/mastodon/live/.env.production ]; then
echo " ✓ .env.production exists"
# Check critical settings
SECRET_KEY=$(grep "^SECRET_KEY_BASE=" /home/mastodon/live/.env.production | cut -d= -f2)
if [ -n "$SECRET_KEY" ] && [ ${#SECRET_KEY} -gt 50 ]; then
echo " ✓ SECRET_KEY_BASE: configured"
else
echo " ✗ SECRET_KEY_BASE: missing or invalid"
FAILED=1
fi
VAPID_KEY=$(grep "^VAPID_PRIVATE_KEY=" /home/mastodon/live/.env.production | cut -d= -f2)
if [ -n "$VAPID_KEY" ]; then
echo " ✓ VAPID keys: configured"
else
echo " ✗ VAPID keys: missing"
WARN=1
fi
else
echo " ✗ .env.production: not found"
FAILED=1
fi
echo ""
echo "=========================================="
if [ $FAILED -eq 0 ] && [ $WARN -eq 0 ]; then
echo "✅ All checks passed!"
elif [ $FAILED -eq 0 ]; then
echo "⚠️ Passed with warnings"
else
echo "❌ Some checks failed"
fi
echo "=========================================="
exit $FAILED