#!/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 </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" </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_CONF" </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"