a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
399 lines
12 KiB
Bash
399 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### deploy-freshrss.sh — Deploy FreshRSS with Docker Compose + Nginx reverse proxy ####
|
|
#### Checks for existing configs, never overwrites. Safe to run on existing setups. ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### sudo ./deploy-freshrss.sh --domain rss.example.com ####
|
|
#### sudo ./deploy-freshrss.sh --domain rss.example.com --port 8081 ####
|
|
#### sudo ./deploy-freshrss.sh --dry-run --domain rss.example.com ####
|
|
#### sudo ./deploy-freshrss.sh --remove ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
DOMAIN=""
|
|
PORT="8080"
|
|
INSTALL_DIR="/opt/freshrss"
|
|
DB_PASSWORD=""
|
|
TZ="UTC"
|
|
CRON_MIN="*/15"
|
|
DRY_RUN=false
|
|
REMOVE=false
|
|
SKIP_NGINX=false
|
|
SKIP_SSL=false
|
|
|
|
# Colors
|
|
if [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BOLD='\033[1m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" BOLD="" RESET=""
|
|
fi
|
|
|
|
log() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
info() { echo -e "${BOLD}[INFO]${RESET} $*"; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Deploy FreshRSS with Docker Compose, PostgreSQL, and Nginx reverse proxy.
|
|
|
|
Installs:
|
|
1. Docker Compose stack (FreshRSS + PostgreSQL) in /opt/freshrss/
|
|
2. Nginx reverse proxy config in /etc/nginx/conf.d/
|
|
3. Let's Encrypt SSL certificate (optional)
|
|
|
|
Options:
|
|
--domain DOMAIN Domain/subdomain for FreshRSS (required for install)
|
|
--port PORT Local port for FreshRSS container (default: 8080)
|
|
--install-dir PATH Installation directory (default: /opt/freshrss)
|
|
--db-password PASS PostgreSQL password (default: auto-generated)
|
|
--timezone TZ Timezone (default: UTC)
|
|
--cron-min PATTERN Feed update schedule (default: */15)
|
|
--skip-nginx Skip Nginx config (manual reverse proxy)
|
|
--skip-ssl Skip Let's Encrypt certificate
|
|
--dry-run Show what would be done without making changes
|
|
--remove Remove FreshRSS deployment
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
$(basename "$0") --domain rss.example.com
|
|
$(basename "$0") --domain rss.example.com --port 8081 --timezone America/Chicago
|
|
$(basename "$0") --domain rss.example.com --skip-ssl
|
|
$(basename "$0") --dry-run --domain rss.example.com
|
|
$(basename "$0") --remove
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--domain) DOMAIN="$2"; shift ;;
|
|
--port) PORT="$2"; shift ;;
|
|
--install-dir) INSTALL_DIR="$2"; shift ;;
|
|
--db-password) DB_PASSWORD="$2"; shift ;;
|
|
--timezone) TZ="$2"; shift ;;
|
|
--cron-min) CRON_MIN="$2"; shift ;;
|
|
--skip-nginx) SKIP_NGINX=true ;;
|
|
--skip-ssl) SKIP_SSL=true ;;
|
|
--dry-run) DRY_RUN=true ;;
|
|
--remove) REMOVE=true ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) err "Unknown option: $1"; usage; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ $EUID -ne 0 ]]; then
|
|
err "Must run as root (sudo)"
|
|
exit 1
|
|
fi
|
|
|
|
# -- Remove mode --
|
|
|
|
if [[ "$REMOVE" == "true" ]]; then
|
|
info "Removing FreshRSS deployment..."
|
|
echo ""
|
|
|
|
if [[ -f "${INSTALL_DIR}/docker-compose.yml" ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would run: docker compose down in ${INSTALL_DIR}"
|
|
else
|
|
cd "$INSTALL_DIR"
|
|
docker compose down 2>/dev/null || true
|
|
log "Stopped FreshRSS containers"
|
|
fi
|
|
fi
|
|
|
|
# Remove nginx config
|
|
for f in /etc/nginx/conf.d/freshrss.conf /etc/nginx/sites-enabled/freshrss.conf /etc/nginx/sites-available/freshrss.conf; do
|
|
if [[ -f "$f" ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would remove: $f"
|
|
else
|
|
rm -f "$f"
|
|
log "Removed $f"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ "$DRY_RUN" != "true" ]] && command -v nginx &>/dev/null; then
|
|
nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null && log "Reloaded Nginx"
|
|
fi
|
|
|
|
echo ""
|
|
if [[ "$DRY_RUN" != "true" ]]; then
|
|
log "Containers stopped and Nginx config removed."
|
|
info "Data preserved at ${INSTALL_DIR}/ - remove manually if desired:"
|
|
echo " rm -rf ${INSTALL_DIR}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# -- Validation --
|
|
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
err "Domain is required: --domain rss.example.com"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v docker &>/dev/null; then
|
|
err "Docker is not installed. Install Docker first."
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker compose version &>/dev/null 2>&1; then
|
|
err "Docker Compose v2 is not available. Install docker-compose-plugin."
|
|
exit 1
|
|
fi
|
|
|
|
# Generate DB password if not provided
|
|
if [[ -z "$DB_PASSWORD" ]]; then
|
|
DB_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
|
|
fi
|
|
|
|
# -- Install mode --
|
|
|
|
info "Deploying FreshRSS..."
|
|
echo ""
|
|
info "Domain: ${DOMAIN}"
|
|
info "Port: ${PORT}"
|
|
info "Install dir: ${INSTALL_DIR}"
|
|
info "Timezone: ${TZ}"
|
|
info "Feed cron: ${CRON_MIN}"
|
|
echo ""
|
|
|
|
# 1. Create directory
|
|
if [[ -d "$INSTALL_DIR" ]]; then
|
|
info "Directory ${INSTALL_DIR} already exists"
|
|
else
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would create: ${INSTALL_DIR}"
|
|
else
|
|
mkdir -p "$INSTALL_DIR"
|
|
log "Created ${INSTALL_DIR}"
|
|
fi
|
|
fi
|
|
|
|
# 2. Docker Compose file
|
|
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
|
|
|
|
if [[ -f "$COMPOSE_FILE" ]]; then
|
|
info "docker-compose.yml already exists - skipping (delete to recreate)"
|
|
else
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would create: ${COMPOSE_FILE}"
|
|
else
|
|
cat > "$COMPOSE_FILE" <<YAML
|
|
services:
|
|
freshrss:
|
|
image: freshrss/freshrss:latest
|
|
container_name: freshrss
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${PORT}:80"
|
|
volumes:
|
|
- ./data:/var/www/FreshRSS/data
|
|
- ./extensions:/var/www/FreshRSS/extensions
|
|
environment:
|
|
- TZ=${TZ}
|
|
- CRON_MIN=${CRON_MIN}
|
|
depends_on:
|
|
freshrss-db:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:80"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
|
|
freshrss-db:
|
|
image: postgres:16-alpine
|
|
container_name: freshrss-db
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./postgres-data:/var/lib/postgresql/data
|
|
environment:
|
|
- POSTGRES_DB=freshrss
|
|
- POSTGRES_USER=freshrss
|
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U freshrss"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
YAML
|
|
log "Created ${COMPOSE_FILE}"
|
|
fi
|
|
fi
|
|
|
|
# 3. Start containers
|
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^freshrss$'; then
|
|
info "FreshRSS container is already running"
|
|
else
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would run: docker compose up -d"
|
|
else
|
|
cd "$INSTALL_DIR"
|
|
docker compose up -d
|
|
log "Started FreshRSS containers"
|
|
fi
|
|
fi
|
|
|
|
# 4. Nginx reverse proxy
|
|
if [[ "$SKIP_NGINX" == "true" ]]; then
|
|
info "Skipping Nginx config (--skip-nginx)"
|
|
elif ! command -v nginx &>/dev/null; then
|
|
warn "Nginx not installed - skipping reverse proxy config"
|
|
SKIP_NGINX=true
|
|
else
|
|
NGINX_CONF=""
|
|
# Detect config directory style
|
|
if [[ -d /etc/nginx/conf.d ]]; then
|
|
NGINX_CONF="/etc/nginx/conf.d/freshrss.conf"
|
|
elif [[ -d /etc/nginx/sites-available ]]; then
|
|
NGINX_CONF="/etc/nginx/sites-available/freshrss.conf"
|
|
else
|
|
warn "Could not detect Nginx config directory - skipping"
|
|
SKIP_NGINX=true
|
|
fi
|
|
|
|
if [[ "$SKIP_NGINX" != "true" && -n "$NGINX_CONF" ]]; then
|
|
if [[ -f "$NGINX_CONF" ]]; then
|
|
info "Nginx config already exists at ${NGINX_CONF} - skipping"
|
|
else
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would create: ${NGINX_CONF}"
|
|
else
|
|
if [[ "$SKIP_SSL" == "true" ]]; then
|
|
# HTTP only
|
|
cat > "$NGINX_CONF" <<NGINX
|
|
server {
|
|
listen 80;
|
|
server_name ${DOMAIN};
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:${PORT};
|
|
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 \$scheme;
|
|
}
|
|
}
|
|
NGINX
|
|
else
|
|
# HTTPS with placeholder (certbot will fill in)
|
|
cat > "$NGINX_CONF" <<NGINX
|
|
server {
|
|
listen 80;
|
|
server_name ${DOMAIN};
|
|
return 301 https://\$host\$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name ${DOMAIN};
|
|
|
|
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:${PORT};
|
|
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 \$scheme;
|
|
}
|
|
}
|
|
NGINX
|
|
fi
|
|
|
|
log "Created ${NGINX_CONF}"
|
|
|
|
# Symlink if sites-available style
|
|
if [[ "$NGINX_CONF" == *sites-available* ]]; then
|
|
ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/freshrss.conf
|
|
log "Symlinked to sites-enabled"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 5. SSL certificate
|
|
if [[ "$SKIP_SSL" != "true" && "$SKIP_NGINX" != "true" ]]; then
|
|
if [[ -d "/etc/letsencrypt/live/${DOMAIN}" ]]; then
|
|
info "SSL certificate already exists for ${DOMAIN}"
|
|
elif command -v certbot &>/dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
info "Would run: certbot certonly --nginx -d ${DOMAIN}"
|
|
else
|
|
certbot certonly --nginx -d "$DOMAIN" --non-interactive --agree-tos --register-unsafely-without-email || {
|
|
warn "Certbot failed - configure SSL manually"
|
|
warn "Run: certbot certonly --nginx -d ${DOMAIN}"
|
|
}
|
|
fi
|
|
else
|
|
warn "certbot not installed - configure SSL manually"
|
|
fi
|
|
fi
|
|
|
|
# 6. Reload Nginx
|
|
if [[ "$SKIP_NGINX" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
if nginx -t 2>/dev/null; then
|
|
systemctl reload nginx
|
|
log "Reloaded Nginx"
|
|
else
|
|
warn "Nginx config test failed - check config manually"
|
|
fi
|
|
fi
|
|
|
|
# -- Summary --
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Deployment summary:${RESET}"
|
|
echo " Docker Compose: ${INSTALL_DIR}/docker-compose.yml"
|
|
echo " FreshRSS: http://127.0.0.1:${PORT}"
|
|
if [[ "$SKIP_NGINX" != "true" ]]; then
|
|
if [[ "$SKIP_SSL" == "true" ]]; then
|
|
echo " Public URL: http://${DOMAIN}"
|
|
else
|
|
echo " Public URL: https://${DOMAIN}"
|
|
fi
|
|
echo " Nginx config: ${NGINX_CONF:-/etc/nginx/conf.d/freshrss.conf}"
|
|
fi
|
|
echo " Database: PostgreSQL (freshrss-db container)"
|
|
echo " Feed updates: Every ${CRON_MIN} minutes"
|
|
echo " Data directory: ${INSTALL_DIR}/data/"
|
|
echo ""
|
|
echo -e "${BOLD}Next steps:${RESET}"
|
|
if [[ "$SKIP_SSL" == "true" ]]; then
|
|
echo " 1. Open http://${DOMAIN} and complete the setup wizard"
|
|
else
|
|
echo " 1. Open https://${DOMAIN} and complete the setup wizard"
|
|
fi
|
|
echo " 2. Database config in wizard:"
|
|
echo " Type: PostgreSQL"
|
|
echo " Host: freshrss-db"
|
|
echo " Database: freshrss"
|
|
echo " User: freshrss"
|
|
echo " Password: (saved in ${INSTALL_DIR}/docker-compose.yml)"
|
|
echo " 3. Create your admin account"
|
|
echo " 4. Add your first feed: https://mylinux.work/index.xml"
|
|
echo ""
|
|
info "Remove with: $(basename "$0") --remove"
|