Compare commits
29 Commits
feature/tr
...
ba889f9c38
| Author | SHA1 | Date | |
|---|---|---|---|
| ba889f9c38 | |||
| 026d24a3ae | |||
| 2ae84f4481 | |||
| f3fab15ffb | |||
| db968ba5ca | |||
| 191d25e281 | |||
| f07d0937d9 | |||
| 749aa6f1cf | |||
| f4409eb258 | |||
| a74707dc1f | |||
| 5d7a162647 | |||
| a0e63e2e2b | |||
| 2fadb08c72 | |||
| 6d2baa7300 | |||
| 13b73671f8 | |||
| 2217377ae8 | |||
| ca4c3e92f0 | |||
| 3ce92b7394 | |||
| 4ad7bf0a38 | |||
| 6d9139408d | |||
| 6e17920cfd | |||
| 6b1a8b7d45 | |||
| 09b20f71fc | |||
| afbffb97e3 | |||
| 1c5959cafb | |||
| 73e40af91a | |||
| 461a0dc110 | |||
| 91873062c9 | |||
| 8a52e6894f |
27
.env.example
27
.env.example
@@ -36,9 +36,10 @@ TAILSCALE_TAGS=tag:nas
|
|||||||
# Enable Tailscale Funnel (public access) for HTTPS? Set to 'true' or 'false'. 'false' uses Serve (Tailnet only, recommended).
|
# Enable Tailscale Funnel (public access) for HTTPS? Set to 'true' or 'false'. 'false' uses Serve (Tailnet only, recommended).
|
||||||
ENABLE_FUNNEL_HTTPS=false
|
ENABLE_FUNNEL_HTTPS=false
|
||||||
|
|
||||||
# --- Primary Hostname ---
|
# --- Primary Application Hostname ---
|
||||||
# Primary hostname used by Traefik for routing. Derived from Tailscale settings by default.
|
# Primary hostname used by Traefik/Authelia. Derived from Tailscale settings by default.
|
||||||
HOSTNAME=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}
|
# Renamed from HOSTNAME to avoid collision with host system environment variable.
|
||||||
|
APP_HOSTNAME=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}
|
||||||
|
|
||||||
# --- Application Credentials ---
|
# --- Application Credentials ---
|
||||||
# qBittorrent Web UI Credentials (change default!)
|
# qBittorrent Web UI Credentials (change default!)
|
||||||
@@ -62,18 +63,16 @@ HOMEPAGE_VAR_WEATHER_UNIT=metric
|
|||||||
|
|
||||||
# --- Authelia Settings ---
|
# --- Authelia Settings ---
|
||||||
# Generate strong random secrets for these using tools like `openssl rand -hex 32`
|
# Generate strong random secrets for these using tools like `openssl rand -hex 32`
|
||||||
AUTHELIA_JWT_SECRET= # Example: your_strong_jwt_secret
|
# These are all REQUIRED for Authelia to function properly
|
||||||
AUTHELIA_SESSION_SECRET= # Example: your_strong_session_secret
|
AUTHELIA_JWT_SECRET= # Secret used for JWT tokens (password reset, etc)
|
||||||
AUTHELIA_STORAGE_ENCRYPTION_KEY= # Example: your_strong_storage_encryption_key
|
AUTHELIA_SESSION_SECRET= # Secret for encrypting session cookies
|
||||||
AUTHELIA_REDIS_PASSWORD= # Example: your_strong_redis_password
|
AUTHELIA_STORAGE_ENCRYPTION_KEY= # Secret for encrypting stored data
|
||||||
|
AUTHELIA_REDIS_PASSWORD= # Password for Redis session storage
|
||||||
|
|
||||||
# Google OIDC Provider Settings (Get from Google Cloud Console - https://console.cloud.google.com/apis/credentials)
|
# Note: The following variables are no longer needed with Authelia 4.38+ and the updated configuration
|
||||||
AUTHELIA_GOOGLE_OIDC_CLIENT_ID= # Example: your-google-client-id.apps.googleusercontent.com
|
# They are preserved for backward compatibility but will be automatically mapped to the new structure
|
||||||
AUTHELIA_GOOGLE_OIDC_CLIENT_SECRET= # Example: GOCSPX-your-google-client-secret
|
# AUTHELIA_SESSION_DOMAIN=${APP_HOSTNAME}
|
||||||
|
# AUTHELIA_DEFAULT_REDIRECT_URL=https://${APP_HOSTNAME}/home
|
||||||
# Authelia Session Configuration
|
|
||||||
AUTHELIA_SESSION_DOMAIN=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}
|
|
||||||
AUTHELIA_DEFAULT_REDIRECT_URL=https://${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/home
|
|
||||||
|
|
||||||
# --- API Keys & Integration Tokens (Optional - Mainly for Homepage Widgets) ---
|
# --- API Keys & Integration Tokens (Optional - Mainly for Homepage Widgets) ---
|
||||||
# Find API keys within each application's settings (usually Settings > General or Security)
|
# Find API keys within each application's settings (usually Settings > General or Security)
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
|||||||
*.env
|
*.env
|
||||||
|
*.bak
|
||||||
.idea
|
.idea
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
/authelia/*.yml
|
||||||
|
!/authelia/*.example.yml
|
||||||
/homepage/logs
|
/homepage/logs
|
||||||
/homepage/*.yaml
|
/homepage/*.yaml
|
||||||
/homepage/*.css
|
/homepage/*.css
|
||||||
|
|||||||
642
README.md
642
README.md
@@ -4,228 +4,415 @@ This project provides a comprehensive, self-hosted media and utility server setu
|
|||||||
|
|
||||||
The core idea is to manage media libraries (movies, TV shows, music), automate downloads securely, provide easy access via a dashboard, and enable remote access through Tailscale.
|
The core idea is to manage media libraries (movies, TV shows, music), automate downloads securely, provide easy access via a dashboard, and enable remote access through Tailscale.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Docker Compose NAS](#docker-compose-nas)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Architecture Overview](#architecture-overview)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Required Setup Steps](#required-setup-steps)
|
||||||
|
- [Quick Start Guide](#quick-start-guide)
|
||||||
|
- [Configuration (`.env` Variables)](#configuration-env-variables)
|
||||||
|
- [Core System \& Paths](#core-system--paths)
|
||||||
|
- [Networking \& Access (Tailscale)](#networking--access-tailscale)
|
||||||
|
- [Authentication (Authelia)](#authentication-authelia)
|
||||||
|
- [Service Credentials](#service-credentials)
|
||||||
|
- [Homepage Customization \& Widgets](#homepage-customization--widgets)
|
||||||
|
- [Optional Features \& Services](#optional-features--services)
|
||||||
|
- [Detailed Setup \& Usage](#detailed-setup--usage)
|
||||||
|
- [Authelia User Management](#authelia-user-management)
|
||||||
|
- [(Optional) VPN Configuration](#optional-vpn-configuration)
|
||||||
|
- [(Optional) Traefik DNS Challenge](#optional-traefik-dns-challenge)
|
||||||
|
- [Service Access](#service-access)
|
||||||
|
- [Setup Script Commands (`update-setup.sh`)](#setup-script-commands-update-setupsh)
|
||||||
|
- [Managing Service Authentication](#managing-service-authentication)
|
||||||
|
- [Optional Services](#optional-services)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Middleware Not Found Errors](#middleware-not-found-errors)
|
||||||
|
- [SELinux Socket Permissions (Docker)](#selinux-socket-permissions-docker)
|
||||||
|
- [Authelia v4.38+ Configuration](#authelia-v438-configuration)
|
||||||
|
- [Tailscale Issues](#tailscale-issues)
|
||||||
|
- [File Permissions](#file-permissions)
|
||||||
|
- [Advanced Topics](#advanced-topics)
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This stack uses a combination of key services for routing, access, and security:
|
||||||
|
|
||||||
|
* **[Tailscale](https://tailscale.com):** Provides a secure overlay network (WireGuard-based VPN) connecting your devices. It allows access to the NAS services from anywhere without opening firewall ports and handles HTTPS termination via its built-in `tailscale serve` or `tailscale funnel` features. All other services run within Tailscale's network namespace.
|
||||||
|
* **[Traefik](https://traefik.io):** Acts as a reverse proxy *within* the Tailscale network. It discovers services via Docker labels and routes incoming requests (from Tailscale) to the appropriate container based on paths (e.g., `/sonarr`, `/radarr`).
|
||||||
|
* **[Authelia](https://www.authelia.com):** Serves as the authentication gateway. Traefik forwards requests to Authelia for verification. If a user isn't logged in, they are redirected to the Authelia portal (`/`). Once authenticated, Authelia sets a session cookie (stored in Redis), and Traefik allows access to the requested service. You can configure which services require authentication via environment variables.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
This stack includes:
|
This stack includes the following services, categorized for clarity:
|
||||||
|
|
||||||
* **Reverse Proxy & Service Discovery:** [Traefik](https://traefik.io) automatically routes traffic to services.
|
**Core Infrastructure:**
|
||||||
* **Media Management:**
|
* **Reverse Proxy:** [Traefik](https://traefik.io) - Manages internal routing and service discovery.
|
||||||
* [Sonarr](https://sonarr.tv): TV show management.
|
* **Secure Remote Access:** [Tailscale](https://tailscale.com) - Provides VPN access and HTTPS.
|
||||||
* [Radarr](https://radarr.video): Movie management.
|
* **Authentication:** [Authelia](https://www.authelia.com) & [Redis](https://redis.io) - Single sign-on portal and session management.
|
||||||
* [Lidarr](https://lidarr.audio) (Optional): Music management.
|
* **Dashboard:** [Homepage](https://gethomepage.dev) - Centralized access point (at `/home`).
|
||||||
* [Bazarr](https://www.bazarr.media/): Subtitle management.
|
|
||||||
* **Indexers & Downloads:**
|
**Media Management (\*Arr Suite):**
|
||||||
* [Prowlarr](https://github.com/Prowlarr/Prowlarr): Indexer management for *arr apps.
|
* [Sonarr](https://sonarr.tv): TV show management.
|
||||||
* [qBittorrent](https://www.qbittorrent.org): Bittorrent client (can be configured to run through a VPN).
|
* [Radarr](https://radarr.video): Movie management.
|
||||||
* [SABnzbd](https://sabnzbd.org/) (Optional): Usenet download client.
|
* [Lidarr](https://lidarr.audio) (Optional): Music management.
|
||||||
* **Media Server:** [Jellyfin](https://jellyfin.org) organizes and streams your media.
|
* [Bazarr](https://www.bazarr.media/): Subtitle management.
|
||||||
* **Request Management:** [Jellyseerr](https://github.com/FallenBagel/jellyseerr) allows users (including Jellyfin users) to request media.
|
* [Prowlarr](https://github.com/Prowlarr/Prowlarr): Indexer management for *arr apps.
|
||||||
* **Dashboard:** [Homepage](https://gethomepage.dev) provides a central dashboard to access all services.
|
|
||||||
* **Remote Access:** [Tailscale](https://tailscale.com) provides secure access to your services from anywhere without opening firewall ports. It handles HTTPS termination.
|
**Download Clients:**
|
||||||
* **Utilities:**
|
* [qBittorrent](https://www.qbittorrent.org): Bittorrent client.
|
||||||
* [Watchtower](https://containrrr.dev/watchtower/): Automatically updates running containers to the latest image.
|
* [SABnzbd](https://sabnzbd.org/) (Optional): Usenet download client.
|
||||||
* [Autoheal](https://github.com/willfarrell/docker-autoheal/): Monitors and restarts unhealthy containers.
|
|
||||||
* [Unpackerr](https://unpackerr.zip): Automatically extracts downloaded archives.
|
**Media Serving & Requests:**
|
||||||
* **Other Optional Services:** AdGuard Home, Calibre-Web, Decluttarr, Tandoor Recipes, Joplin Server, Home Assistant, Immich Photos (enable via profiles).
|
* [Jellyfin](https://jellyfin.org): Media server for streaming.
|
||||||
|
* [Jellyseerr](https://github.com/FallenBagel/jellyseerr): Media request management.
|
||||||
|
|
||||||
|
**Utilities:**
|
||||||
|
* [Watchtower](https://containrrr.dev/watchtower/): Automatic container updates.
|
||||||
|
* [Autoheal](https://github.com/willfarrell/docker-autoheal/): Automatic container restarts on failure.
|
||||||
|
* [Unpackerr](https://unpackerr.zip): Automated archive extraction.
|
||||||
|
|
||||||
|
**Optional Services (Enabled via Profiles):**
|
||||||
|
* [AdGuard Home](https://adguard.com/en/adguard-home/overview.html): Network-wide ad blocking.
|
||||||
|
* [Calibre-Web](https://github.com/janeczku/calibre-web): E-book library management.
|
||||||
|
* [Decluttarr](https://github.com/manimatter/decluttarr): Automated download cleanup.
|
||||||
|
* [Tandoor Recipes](https://docs.tandoor.dev/): Recipe management.
|
||||||
|
* [Joplin Server](https://joplinapp.org/): Note-taking synchronization server.
|
||||||
|
* [Home Assistant](https://www.home-assistant.io/): Home automation platform.
|
||||||
|
* [Immich](https://immich.app/): Self-hosted photo and video backup.
|
||||||
|
* [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr): Bypasses Cloudflare challenges (e.g., for Prowlarr).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
* **Linux Host:** Any recent Linux distribution capable of running Docker. Tested on Ubuntu Server 22.04.
|
* **Linux Host:** A system capable of running Docker (e.g., Ubuntu, Debian, Fedora).
|
||||||
* **Docker Engine:** Install the latest version of Docker Engine. [Official Installation Guide](https://docs.docker.com/engine/install/).
|
* **Docker & Docker Compose:** Latest versions installed. See [Docker Engine Install](https://docs.docker.com/engine/install/) and [Docker Compose Install](https://docs.docker.com/compose/install/).
|
||||||
* **Docker Compose V2:** Ensure you have Docker Compose V2 (usually installed as a Docker plugin, invoked via `docker compose`). [Official Installation Guide](https://docs.docker.com/compose/install/).
|
* **User Permissions:** Ability to run `docker` commands (user in `docker` group or use `sudo`).
|
||||||
* **User Permissions:** You'll need a user account that can run `docker` commands (usually by adding the user to the `docker` group) or run `docker compose` via `sudo`.
|
* **Basic Linux Knowledge:** Familiarity with command line, text editors, and file permissions.
|
||||||
* **SELinux (If Enabled):** If your host uses SELinux (e.g., Fedora, CentOS, RHEL), you might need additional host configuration. See the [Troubleshooting](#selinux-socket-permissions) section.
|
* **Tailscale Account:** Required for remote access. [Sign up here](https://tailscale.com/login).
|
||||||
|
* **(Optional) SELinux:** If enabled, see [Troubleshooting](#selinux-socket-permissions).
|
||||||
|
|
||||||
## Quick Start
|
## Required Setup Steps
|
||||||
|
|
||||||
1. **Clone the Repository:**
|
These steps are **mandatory** for a working installation. Without properly completing these, the stack will not function correctly.
|
||||||
|
|
||||||
|
1. **⚠️ Required: System User Information**
|
||||||
|
- Set `USER_ID` and `GROUP_ID` in `.env` (run `id -u` and `id -g` to get yours)
|
||||||
|
- Incorrect values will cause permission errors with mounted volumes
|
||||||
|
|
||||||
|
2. **⚠️ Required: Directory Paths**
|
||||||
|
- Set `CONFIG_ROOT` (where service configurations will be stored)
|
||||||
|
- Set `DATA_ROOT` (where your media libraries will be stored)
|
||||||
|
- Set `DOWNLOAD_ROOT` (must be on same filesystem as DATA_ROOT for hardlinks)
|
||||||
|
|
||||||
|
3. **⚠️ Required: Tailscale Configuration**
|
||||||
|
- Create a Tailscale account at [tailscale.com](https://tailscale.com)
|
||||||
|
- Generate an Auth Key in the [Tailscale Admin Console](https://login.tailscale.com/admin/settings/keys)
|
||||||
|
- Set `TAILSCALE_AUTHKEY` in `.env`
|
||||||
|
- Set `TAILSCALE_TAILNET_DOMAIN` to your Tailnet domain (e.g., `your-name.ts.net`)
|
||||||
|
|
||||||
|
4. **⚠️ Required: Security Credentials**
|
||||||
|
- Generate four strong random secrets using `openssl rand -hex 32`:
|
||||||
|
```bash
|
||||||
|
echo "AUTHELIA_JWT_SECRET=$(openssl rand -hex 32)"
|
||||||
|
echo "AUTHELIA_SESSION_SECRET=$(openssl rand -hex 32)"
|
||||||
|
echo "AUTHELIA_STORAGE_ENCRYPTION_KEY=$(openssl rand -hex 32)"
|
||||||
|
echo "AUTHELIA_REDIS_PASSWORD=$(openssl rand -hex 32)"
|
||||||
|
```
|
||||||
|
- Add these to your `.env` file as shown
|
||||||
|
|
||||||
|
5. **⚠️ Required: Create Admin Password**
|
||||||
|
- Generate a password hash for Authelia:
|
||||||
|
```bash
|
||||||
|
docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password'
|
||||||
|
```
|
||||||
|
- Replace the example hash in `authelia/users_database.yml` with your generated hash
|
||||||
|
|
||||||
|
6. **⚠️ Optional: Authentication Configuration**
|
||||||
|
- Choose which services require authentication by setting the corresponding variables in your `.env` file:
|
||||||
|
```
|
||||||
|
AUTH_JELLYFIN=false # Example: Allow Jellyfin access without authentication
|
||||||
|
AUTH_SONARR=true # Example: Require authentication for Sonarr
|
||||||
|
```
|
||||||
|
- By default, all services require authentication if not specified otherwise
|
||||||
|
|
||||||
|
## Quick Start Guide
|
||||||
|
|
||||||
|
After completing all [Required Setup Steps](#required-setup-steps) above, follow these steps to get up and running:
|
||||||
|
|
||||||
|
1. **Clone Repository:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AdrienPoupa/docker-compose-nas.git
|
||||||
|
cd docker-compose-nas
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Environment:**
|
||||||
|
```bash
|
||||||
|
# Create an .env file from the example
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit the .env file and fill in ALL required values
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Authelia Admin:**
|
||||||
|
```bash
|
||||||
|
# Generate a password hash
|
||||||
|
docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password'
|
||||||
|
|
||||||
|
# Edit the users_database.yml with the generated hash
|
||||||
|
nano authelia/users_database.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the Setup Script:**
|
||||||
|
```bash
|
||||||
|
# Make the script executable
|
||||||
|
chmod +x ./update-setup.sh
|
||||||
|
|
||||||
|
# Run the setup tool (use 'all' for initial setup)
|
||||||
|
./update-setup.sh all
|
||||||
|
```
|
||||||
|
This script will:
|
||||||
|
- Update your `.env` file while preserving existing values (`update-env`).
|
||||||
|
- Configure Authelia with your Tailscale domain settings (`update-authelia`).
|
||||||
|
- Set up service configurations and retrieve API keys (`update-services`).
|
||||||
|
|
||||||
|
You can also run individual commands like `./update-setup.sh update-authelia`. Run `./update-setup.sh help` for all options.
|
||||||
|
|
||||||
|
5. **Start the Stack:**
|
||||||
|
```bash
|
||||||
|
# Start containers
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
*(Wait for containers to download and start)*
|
||||||
|
|
||||||
|
6. **Access Your NAS:**
|
||||||
|
- Open `https://<TAILSCALE_HOSTNAME>.<TAILSCALE_TAILNET_DOMAIN>/`
|
||||||
|
- Log in with username `admin` and the password you set
|
||||||
|
- After login, you'll land on the Homepage dashboard at `/home`
|
||||||
|
|
||||||
|
> ⚠️ **IMPORTANT:** If the stack fails to start, check the [Troubleshooting](#troubleshooting) section and verify you've properly completed all [Required Setup Steps](#required-setup-steps).
|
||||||
|
|
||||||
|
## Configuration (`.env` Variables)
|
||||||
|
|
||||||
|
This file controls essential settings. Copy `.env.example` to `.env` and modify the values. **Bold variables** are critical for initial setup.
|
||||||
|
|
||||||
|
#### Core System & Paths
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
|
||||||
|
| **`USER_ID`** | Linux user ID for container permissions. Find with `id -u`. | `1000` |
|
||||||
|
| **`GROUP_ID`** | Linux group ID for container permissions. Find with `id -g`. | `1000` |
|
||||||
|
| **`TIMEZONE`** | Your local timezone (e.g., `America/New_York`, `Asia/Manila`). [List](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). | `America/New_York` |
|
||||||
|
| `CONFIG_ROOT` | Host base directory for service configurations. `.` = project subdirs. | `.` |
|
||||||
|
| `DATA_ROOT` | Host directory for media libraries (movies, TV, music, etc.). | `/mnt/data` |
|
||||||
|
| `DOWNLOAD_ROOT` | Host directory for downloads (in progress/completed). **Must be on same filesystem as `DATA_ROOT` for hardlinks.** | `/mnt/data/torrents` |
|
||||||
|
| `IMMICH_UPLOAD_LOCATION` | Host path for Immich uploads (if `immich` profile enabled). | `/mnt/data/photos` |
|
||||||
|
|
||||||
|
#### Networking & Access (Tailscale)
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
|
||||||
|
| **`TAILSCALE_AUTHKEY`** | **Required.** Auth key from [Tailscale Admin Console](https://login.tailscale.com/admin/settings/keys). Use reusable or ephemeral. | *(None)* |
|
||||||
|
| **`TAILSCALE_TAILNET_DOMAIN`**| **Required.** Your unique Tailnet domain (e.g., `your-name.ts.net`). | `your-tailnet.ts.net` |
|
||||||
|
| `TAILSCALE_HOSTNAME` | Desired hostname for this NAS within Tailscale. | `tailscale-nas` |
|
||||||
|
| `TAILSCALE_TAGS` | Optional tags for the Tailscale node (e.g., `tag:nas`). | `tag:nas` |
|
||||||
|
| `ENABLE_FUNNEL_HTTPS` | Use Tailscale Funnel (`true` = public access via Tailscale domain) or Serve (`false` = Tailnet-only access, recommended). | `false` |
|
||||||
|
| `APP_HOSTNAME` | Primary hostname used by Traefik/Authelia. Defaults to Tailscale FQDN. Renamed from `HOSTNAME` to avoid host system conflicts. Can be overridden if using custom DNS. | `${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}` |
|
||||||
|
|
||||||
|
#### Authentication (Authelia)
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
|
||||||
|
| **`AUTHELIA_JWT_SECRET`** | **Required.** Random secret for Authelia (used for password reset JWT). **Generate your own!** | *(None - Example in file)* |
|
||||||
|
| **`AUTHELIA_SESSION_SECRET`** | **Required.** Random secret for session cookies. **Generate your own!** | *(None - Example in file)* |
|
||||||
|
| **`AUTHELIA_STORAGE_ENCRYPTION_KEY`** | **Required.** Random secret for encrypting data at rest (e.g., SQLite DB). **Generate your own!** | *(None - Example in file)* |
|
||||||
|
| **`AUTHELIA_REDIS_PASSWORD`** | **Required.** Password for the Redis database (used for session storage). **Generate your own!** | *(None - Example in file)* |
|
||||||
|
| `AUTH_SONARR` | Control whether Sonarr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_RADARR` | Control whether Radarr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_BAZARR` | Control whether Bazarr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_PROWLARR` | Control whether Prowlarr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_JELLYSEERR` | Control whether Jellyseerr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_QBITTORRENT` | Control whether qBittorrent requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_LIDARR` | Control whether Lidarr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_JELLYFIN` | Control whether Jellyfin requires authentication (`true`/`false`). | `false` |
|
||||||
|
| `AUTH_HOMEPAGE` | Control whether Homepage requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_FLARESOLVERR` | Control whether FlareSolverr requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_SABNZBD` | Control whether SABnzbd requires authentication (`true`/`false`). | `true` |
|
||||||
|
| `AUTH_CALIBRE` | Control whether Calibre-Web requires authentication (`true`/`false`). | `true` |
|
||||||
|
|
||||||
|
> **Note:** Authentication variables were introduced to give you fine-grained control over which services require login. Set to `false` for services you want to access without authentication.
|
||||||
|
|
||||||
|
#### Service Credentials
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- |
|
||||||
|
| `QBITTORRENT_USERNAME` | Username for qBittorrent Web UI. | `admin` |
|
||||||
|
| `QBITTORRENT_PASSWORD` | Password for qBittorrent Web UI. **Change default!** (May need to use temp password from logs on first run, then change in UI & `.env`). | `adminadmin` |
|
||||||
|
| `CALIBRE_USERNAME` | Username for Calibre-Web (if `calibre-web` profile enabled). | `admin` |
|
||||||
|
| `CALIBRE_PASSWORD` | Password for Calibre-Web (if `calibre-web` profile enabled). | `admin123` |
|
||||||
|
| `IMMICH_DB_PASSWORD` | Password for Immich's internal database (if `immich` profile enabled). | `postgres` |
|
||||||
|
| `ADGUARD_USERNAME` | Username for AdGuard Home (if `adguardhome` profile enabled). | *(None)* |
|
||||||
|
| `ADGUARD_PASSWORD` | Password for AdGuard Home (if `adguardhome` profile enabled). | *(None)* |
|
||||||
|
|
||||||
|
#### Homepage Customization & Widgets
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------- |
|
||||||
|
| `HOMEPAGE_VAR_TITLE` | Title shown on the dashboard. | `Docker-Compose NAS` |
|
||||||
|
| `HOMEPAGE_VAR_SEARCH_PROVIDER` | Default search engine. [Options](https://gethomepage.dev/en/widgets/search/). | `google` |
|
||||||
|
| `HOMEPAGE_VAR_HEADER_STYLE` | Dashboard header style. [Options](https://gethomepage.dev/en/configs/settings/#header-style). | `boxed` |
|
||||||
|
| `HOMEPAGE_VAR_WEATHER_CITY` | City for weather widget. | *(None)* |
|
||||||
|
| `HOMEPAGE_VAR_WEATHER_LAT` | Latitude for weather widget. | *(None)* |
|
||||||
|
| `HOMEPAGE_VAR_WEATHER_LONG` | Longitude for weather widget. | *(None)* |
|
||||||
|
| `HOMEPAGE_VAR_WEATHER_UNIT` | Weather units (`metric` or `imperial`). | `metric` |
|
||||||
|
| `SONARR_API_KEY` | API Keys for various services, primarily used for Homepage widgets. Find keys in each app's settings. | *(None)* |
|
||||||
|
| `RADARR_API_KEY` | " | *(None)* |
|
||||||
|
| `LIDARR_API_KEY` | " (if `lidarr` profile enabled) | *(None)* |
|
||||||
|
| `BAZARR_API_KEY` | " | *(None)* |
|
||||||
|
| `PROWLARR_API_KEY` | " | *(None)* |
|
||||||
|
| `JELLYFIN_API_KEY` | " | *(None)* |
|
||||||
|
| `JELLYSEERR_API_KEY` | " | *(None)* |
|
||||||
|
| `SABNZBD_API_KEY` | " (if `sabnzbd` profile enabled) | *(None)* |
|
||||||
|
| `IMMICH_API_KEY` | " (if `immich` profile enabled) | *(None)* |
|
||||||
|
| `HOMEASSISTANT_ACCESS_TOKEN` | " (if `homeassistant` profile enabled) | *(None)* |
|
||||||
|
|
||||||
|
#### Optional Features & Services
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| :---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
|
||||||
|
| `COMPOSE_PROFILES` | Comma-separated list of optional service profiles to enable (e.g., `lidarr,sabnzbd`). See [Optional Services](#optional-services). | *(None)* |
|
||||||
|
| `COMPOSE_PATH_SEPARATOR` | Path separator for `COMPOSE_FILE` (use `;` for Windows). | `:` |
|
||||||
|
| `COMPOSE_FILE` | Colon-separated list of compose files to use. Allows extending base config. | `docker-compose.yml:...` (See `.env.example`) |
|
||||||
|
| `DECLUTTARR_TEST_RUN` | Run Decluttarr in test mode (`True`/`False`)? (if `decluttarr` profile enabled). | `True` |
|
||||||
|
| `DECLUTTARR_...` | Other Decluttarr settings (see `.env.example`). | *(Varies)* |
|
||||||
|
| `PIA_USER` / `PIA_PASS` | Credentials for PIA VPN (if using default VPN setup for qBittorrent). | *(None)* |
|
||||||
|
| `PIA_LOCATION` | PIA server location (if using default VPN). [List](https://serverlist.piaservers.net/vpninfo/servers/v6). | `ca` |
|
||||||
|
| `PIA_LOCAL_NETWORK` | Your local network CIDR (e.g., `192.168.1.0/24`) to allow local access to VPN'd containers. | `192.168.0.0/16` |
|
||||||
|
| `DNS_CHALLENGE` | Enable Traefik DNS challenge for Let's Encrypt (`true`/`false`). **Not needed if using Tailscale for HTTPS.** | `true` |
|
||||||
|
| `DNS_CHALLENGE_PROVIDER` | Your DNS provider (e.g., `cloudflare`). [Providers](https://doc.traefik.io/traefik/https/acme/#providers). | `cloudflare` |
|
||||||
|
| `LETS_ENCRYPT_EMAIL` | Email for Let's Encrypt (if using DNS challenge). | *(None)* |
|
||||||
|
| `LETS_ENCRYPT_CA_SERVER` | Let's Encrypt server URL (if using DNS challenge). | `https://acme-v02.api.letsencrypt.org/directory` |
|
||||||
|
| `CLOUDFLARE_...` / `PROVIDER_...` | DNS provider API credentials (if using DNS challenge). | *(None)* |
|
||||||
|
| `HOMEASSISTANT_HOSTNAME` | Specific hostname for Home Assistant (if `homeassistant` profile enabled). | *(None)* |
|
||||||
|
| `IMMICH_HOSTNAME` | Specific hostname for Immich (if `immich` profile enabled). | *(None)* |
|
||||||
|
| `ADGUARD_HOSTNAME` | Specific hostname for AdGuard Home (if `adguardhome` profile enabled). | *(None)* |
|
||||||
|
|
||||||
|
## Detailed Setup & Usage
|
||||||
|
|
||||||
|
### Authelia User Management
|
||||||
|
|
||||||
|
Authelia uses the `authelia/users_database.yml` file to manage users.
|
||||||
|
|
||||||
|
* **Setting the Initial Admin Password:**
|
||||||
|
1. As mentioned in the Quick Start, you **must** set a strong password for the default `admin` user.
|
||||||
|
2. Generate a hash using Docker (replace `'your_secure_password'`):
|
||||||
|
```bash
|
||||||
|
docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password'
|
||||||
|
```
|
||||||
|
3. Copy the **entire output hash** (starting with `$argon2id...`).
|
||||||
|
4. Open `authelia/users_database.yml` and replace the example `password:` value under `admin:` with your generated hash.
|
||||||
|
5. Ensure the `admin` user belongs to the `admins` and `users` groups as shown in the example.
|
||||||
|
|
||||||
|
* **Adding More Users:**
|
||||||
|
1. Generate a password hash for the new user as shown above.
|
||||||
|
2. Edit `authelia/users_database.yml`.
|
||||||
|
3. Add a new entry under `users:`, following the format of the `admin` user:
|
||||||
|
```yaml
|
||||||
|
users:
|
||||||
|
admin:
|
||||||
|
# ... (admin details) ...
|
||||||
|
newuser:
|
||||||
|
displayname: "New User Name"
|
||||||
|
password: "paste_generated_hash_here"
|
||||||
|
email: newuser@example.com
|
||||||
|
groups:
|
||||||
|
- users # Add to 'admins' group if needed
|
||||||
|
```
|
||||||
|
4. Save the file and restart Authelia: `docker compose restart authelia`.
|
||||||
|
|
||||||
|
* **Adding/Updating Users (Recommended Method):**
|
||||||
|
Use the setup script's interactive tool:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/AdrienPoupa/docker-compose-nas.git
|
./update-setup.sh manage-accounts
|
||||||
cd docker-compose-nas
|
|
||||||
```
|
```
|
||||||
|
This script handles password hashing and file formatting, reducing the chance of errors. It will prompt you for the username, display name, email, and groups, then generate a secure password hash.
|
||||||
|
|
||||||
2. **Create Configuration File:**
|
* **Enabling User Registration (Optional):**
|
||||||
Copy the example environment file:
|
1. Edit `authelia/configuration.yml`.
|
||||||
```bash
|
2. Find the commented-out `registration:` section near the bottom.
|
||||||
cp .env.example .env
|
3. Uncomment it and set `enable: true`.
|
||||||
```
|
4. Save the file and restart Authelia (`docker compose restart authelia`).
|
||||||
|
5. A "Register" link will now appear on the Authelia login page.
|
||||||
|
|
||||||
3. **Edit `.env` File:**
|
* **Approving Registered Users:**
|
||||||
Open the `.env` file with a text editor and configure it according to your system and preferences. **This is the most crucial step.** See the detailed [Configuration (`.env` File)](#configuration-env-file) section below for explanations of each variable. Minimally, you **must** set `USER_ID`, `GROUP_ID`, `TIMEZONE`, `HOSTNAME`, and `TAILSCALE_AUTHKEY`.
|
1. When a user registers (if enabled), their details are added to `authelia/users_database.yml` but marked as `disabled: true`.
|
||||||
|
2. To approve them, edit `authelia/users_database.yml`.
|
||||||
|
3. Find the new user's entry.
|
||||||
|
4. Change `disabled: true` to `disabled: false` (or simply remove the `disabled: true` line).
|
||||||
|
5. Save the file. The user should now be able to log in.
|
||||||
|
|
||||||
4. **Start the Stack:**
|
### (Optional) VPN Configuration
|
||||||
Run Docker Compose (use `sudo` if your user isn't in the `docker` group):
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
This will pull the necessary images and start all the core services in the background.
|
|
||||||
|
|
||||||
5. **Run Initial Configuration Script:**
|
*(Details about configuring the PIA VPN or other VPN setups could go here if needed.)*
|
||||||
This script helps configure base URLs and API keys within the running *arr applications based on your `.env` file.
|
|
||||||
```bash
|
|
||||||
./update-config.sh
|
|
||||||
```
|
|
||||||
*(Note: You might need to make it executable first: `chmod +x ./update-config.sh`)*
|
|
||||||
|
|
||||||
6. **Access Services:** Once Tailscale is connected, you should be able to access your services via `https://<TAILSCALE_HOSTNAME>.<your-tailnet-name>.ts.net/<service_path>` or `https://<TAILSCALE_IP>/<service_path>`. If you set up DNS for your `HOSTNAME`, you can use `https://<HOSTNAME>/<service_path>`. The main dashboard is at `/`.
|
### (Optional) Traefik DNS Challenge
|
||||||
|
|
||||||
## Configuration (`.env` File)
|
*(Details about setting up DNS provider credentials for Let's Encrypt could go here if needed.)*
|
||||||
|
|
||||||
This file controls all the essential settings for your Docker Compose stack. Copy `.env.example` to `.env` and edit the values.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Core Settings (Required)**
|
|
||||||
|
|
||||||
These are fundamental for basic operation and permissions.
|
|
||||||
|
|
||||||
* `USER_ID`: The Linux user ID that the containers will run as. Find yours with `id -u`.
|
|
||||||
* *Default:* `1000`
|
|
||||||
* `GROUP_ID`: The Linux group ID that the containers will run as. Find yours with `id -g`.
|
|
||||||
* *Default:* `1000`
|
|
||||||
* **Note:** Using the correct IDs is crucial for file permissions, especially for accessing media files on the host.
|
|
||||||
* `TIMEZONE`: Your local timezone (e.g., `America/New_York`, `Europe/London`, `Asia/Manila`). Find yours from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
|
||||||
* *Default:* `America/New_York`
|
|
||||||
* `HOSTNAME`: **(Deprecated - Now derived)** The primary hostname used by Traefik for routing. This is now automatically constructed from `TAILSCALE_HOSTNAME` and `TAILSCALE_TAILNET_DOMAIN`. You generally don't need to set this directly unless overriding the default behavior.
|
|
||||||
* *Default:* `${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Host Paths (Required)**
|
|
||||||
|
|
||||||
Define where container data and configuration are stored on your host machine.
|
|
||||||
|
|
||||||
* `CONFIG_ROOT`: The base directory on your host where configuration files for each service will be stored. Using `.` stores them in subdirectories within the project folder.
|
|
||||||
* *Default:* `.`
|
|
||||||
* `DATA_ROOT`: The main directory on your host containing your media libraries (movies, TV shows, music, books).
|
|
||||||
* *Default:* `/mnt/data`
|
|
||||||
* `DOWNLOAD_ROOT`: The directory on your host where the download client (qBittorrent/SABnzbd) will store downloads in progress and completed files *before* they are imported by *arr apps.
|
|
||||||
* *Default:* `/mnt/data/torrents`
|
|
||||||
* **Hardlink Note:** For efficient storage (avoiding duplicate files), it's highly recommended that `DOWNLOAD_ROOT` is on the **same filesystem** as `DATA_ROOT` (e.g., `/mnt/data/torrents` is inside `/mnt/data`). This allows instant moves via hardlinks instead of slow copies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Tailscale Access (Required)**
|
|
||||||
|
|
||||||
Controls secure remote access via Tailscale.
|
|
||||||
|
|
||||||
* `TAILSCALE_AUTHKEY`: **Required.** An authentication key from your Tailscale account. Generate one in the Tailscale Admin Console under Settings > Keys. You can use a reusable key or an ephemeral key (recommended for containers).
|
|
||||||
* *Default:* (None - **Must be set**)
|
|
||||||
* `TAILSCALE_HOSTNAME`: The desired hostname for this NAS within your Tailscale network.
|
|
||||||
* *Default:* `tailscale-nas`
|
|
||||||
* `TAILSCALE_TAILNET_DOMAIN`: **Required.** The domain of your Tailnet, including your Tailnet's unique name
|
|
||||||
* *Default:* `your-tailnet.ts.net` (**Must be set**)
|
|
||||||
* `TAILSCALE_TAGS`: Optional tags to apply to the Tailscale node (e.g., `tag:nas`).
|
|
||||||
* *Default:* `tag:nas`
|
|
||||||
* `ENABLE_FUNNEL_HTTPS`: Controls Tailscale's public accessibility.
|
|
||||||
* `true`: Enables Tailscale Funnel, making services accessible publicly via the Tailscale domain (`<TAILSCALE_HOSTNAME>.<your-tailnet-name>.ts.net`). Use with caution.
|
|
||||||
* `false`: Uses Tailscale Serve, making services accessible *only* to devices logged into your Tailnet. (Recommended)
|
|
||||||
* *Default:* `false`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Homepage Widgets (Optional)**
|
|
||||||
|
|
||||||
API keys needed *only* if you want to display real-time information from these services on the Homepage dashboard. Find the API keys within each application's settings (usually under Settings > General or Settings > Security).
|
|
||||||
|
|
||||||
* `SONARR_API_KEY`
|
|
||||||
* `RADARR_API_KEY`
|
|
||||||
* `LIDARR_API_KEY` (If Lidarr profile is enabled)
|
|
||||||
* `BAZARR_API_KEY`
|
|
||||||
* `PROWLARR_API_KEY`
|
|
||||||
* `JELLYFIN_API_KEY`
|
|
||||||
* `JELLYSEERR_API_KEY`
|
|
||||||
* `SABNZBD_API_KEY` (If SABnzbd profile is enabled)
|
|
||||||
* `ADGUARD_USERNAME` / `ADGUARD_PASSWORD` (If AdGuard Home profile is enabled)
|
|
||||||
* `CALIBRE_USERNAME` / `CALIBRE_PASSWORD` (If Calibre-Web profile is enabled)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Homepage Customization (Optional)**
|
|
||||||
|
|
||||||
Control the appearance and behavior of the Homepage dashboard.
|
|
||||||
|
|
||||||
* `HOMEPAGE_VAR_TITLE`: Title shown on the dashboard.
|
|
||||||
* *Default:* `Docker-Compose NAS`
|
|
||||||
* `HOMEPAGE_VAR_SEARCH_PROVIDER`: Default search engine. [See options](https://gethomepage.dev/en/widgets/search/).
|
|
||||||
* *Default:* `google`
|
|
||||||
* `HOMEPAGE_VAR_HEADER_STYLE`: Dashboard header style. [See options](https://gethomepage.dev/en/configs/settings/#header-style).
|
|
||||||
* *Default:* `boxed`
|
|
||||||
* `HOMEPAGE_VAR_WEATHER_CITY`, `_LAT`, `_LONG`, `_UNIT`: Configure the weather widget.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Download Client Settings**
|
|
||||||
|
|
||||||
Credentials for included download clients.
|
|
||||||
|
|
||||||
* `QBITTORRENT_USERNAME`: Username for qBittorrent Web UI.
|
|
||||||
* *Default:* `admin`
|
|
||||||
* `QBITTORRENT_PASSWORD`: Password for qBittorrent Web UI.
|
|
||||||
* *Default:* `adminadmin`
|
|
||||||
* **Note:** On first run, qBittorrent might generate a temporary password shown in its logs (`docker compose logs qbittorrent`). Log in with that, change the password in qBittorrent settings, and update this `.env` variable accordingly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **VPN Configuration (Example: PIA - Optional)**
|
|
||||||
|
|
||||||
These variables are specific to the example `thrnz/docker-wireguard-pia` VPN container used for qBittorrent in the default setup. If you use a different VPN provider or container, you'll need different variables. **If you don't use the VPN, you can ignore these.**
|
|
||||||
|
|
||||||
* `PIA_USER`: Private Internet Access username.
|
|
||||||
* `PIA_PASS`: Private Internet Access password.
|
|
||||||
* `PIA_LOCATION`: PIA server location code (e.g., `ca_montreal`, `us_east`). [See list](https://serverlist.piaservers.net/vpninfo/servers/v6).
|
|
||||||
* *Default:* `ca`
|
|
||||||
* `PIA_LOCAL_NETWORK`: Your local network CIDR (e.g., `192.168.1.0/24`). Allows local access to the qBittorrent UI when the VPN is active.
|
|
||||||
* *Default:* `192.168.0.0/16`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Traefik DNS Challenge (Optional)**
|
|
||||||
|
|
||||||
These settings are for enabling automatic HTTPS certificate generation via Let's Encrypt using the DNS-01 challenge method. **This is generally NOT needed** because Tailscale handles HTTPS termination by default in this setup. Only configure this if you have a specific reason to manage your own certificates via Traefik (e.g., accessing services without Tailscale).
|
|
||||||
|
|
||||||
* `DNS_CHALLENGE`: Set to `true` to enable DNS challenge.
|
|
||||||
* *Default:* `true` (Consider setting to `false` if using Tailscale for HTTPS)
|
|
||||||
* `DNS_CHALLENGE_PROVIDER`: Your DNS provider supported by Traefik/Lego (e.g., `cloudflare`, `godaddy`). [See providers](https://doc.traefik.io/traefik/https/acme/#providers).
|
|
||||||
* *Default:* `cloudflare`
|
|
||||||
* `LETS_ENCRYPT_EMAIL`: Your email address for Let's Encrypt notifications.
|
|
||||||
* `LETS_ENCRYPT_CA_SERVER`: Let's Encrypt server URL (use staging for testing).
|
|
||||||
* *Default:* `https://acme-v02.api.letsencrypt.org/directory` (Production)
|
|
||||||
* Provider-Specific Variables (e.g., `CLOUDFLARE_EMAIL`, `CLOUDFLARE_DNS_API_TOKEN`, `CLOUDFLARE_ZONE_API_TOKEN`): Credentials required by your chosen `DNS_CHALLENGE_PROVIDER`. Refer to Traefik documentation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Compose Profiles & Files (Advanced)**
|
|
||||||
|
|
||||||
* `COMPOSE_PROFILES`: Comma-separated list of optional service profiles to enable (e.g., `lidarr,sabnzbd,adguardhome`). See [Optional Services](#optional-services).
|
|
||||||
* `COMPOSE_FILE`: Colon-separated list of compose files to use. Allows extending the base configuration.
|
|
||||||
* *Default:* `docker-compose.yml`
|
|
||||||
|
|
||||||
## Service Access
|
## Service Access
|
||||||
|
|
||||||
With the default Tailscale setup, services are securely accessible via HTTPS using your Tailscale node's name or IP, followed by the service path. Replace `<TAILSCALE_NODE>` with your Tailscale device name (e.g., `tailscale-nas.your-tailnet.ts.net`) or its Tailscale IP address.
|
With the default Tailscale setup and Authelia enabled, services are securely accessible via HTTPS using your Tailscale node's name or IP. Authentication is controlled by the included `update-setup.sh` script.
|
||||||
|
|
||||||
* **Homepage:** `https://<TAILSCALE_NODE>/home`
|
* **Login Portal:** `https://<TAILSCALE_NODE>/` (Redirects unauthenticated users here for secured services)
|
||||||
* **Sonarr:** `https://<TAILSCALE_NODE>/sonarr`
|
* **Homepage Dashboard:** `https://<TAILSCALE_NODE>/home` (Requires login by default)
|
||||||
* **Radarr:** `https://<TAILSCALE_NODE>/radarr`
|
* **Sonarr:** `https://<TAILSCALE_NODE>/sonarr` (Requires login by default)
|
||||||
* **Lidarr:** `https://<TAILSCALE_NODE>/lidarr` (If profile enabled)
|
* **Radarr:** `https://<TAILSCALE_NODE>/radarr` (Requires login by default)
|
||||||
* **Bazarr:** `https://<TAILSCALE_NODE>/bazarr`
|
* **qBittorrent:** `https://<TAILSCALE_NODE>/qbittorrent` (Requires login by default)
|
||||||
* **Jellyseerr:** `https://<TAILSCALE_NODE>/jellyseerr`
|
* **Jellyfin:** `https://<TAILSCALE_NODE>/jellyfin` (Requires login by default)
|
||||||
* **Prowlarr:** `https://<TAILSCALE_NODE>/prowlarr`
|
* ...and so on.
|
||||||
* **qBittorrent:** `https://<TAILSCALE_NODE>/qbittorrent`
|
|
||||||
* **SABnzbd:** `https://<TAILSCALE_NODE>/sabnzbd` (If profile enabled)
|
|
||||||
* **Jellyfin:** `https://<TAILSCALE_NODE>/jellyfin`
|
|
||||||
* **Calibre-Web:** `https://<TAILSCALE_NODE>/calibre` (If profile enabled)
|
|
||||||
* **AdGuard Home:** `http://<TAILSCALE_NODE_IP>:3000` (If profile enabled, access via IP/port initially)
|
|
||||||
* **Tandoor Recipes:** `https://<TAILSCALE_NODE>/recipes` (If profile enabled)
|
|
||||||
* **Joplin Server:** `https://<TAILSCALE_NODE>/joplin` (If profile enabled)
|
|
||||||
* **Home Assistant:** `http://<TAILSCALE_NODE_IP>:8123` (If profile enabled, access via IP/port initially)
|
|
||||||
* **Immich:** `http://<TAILSCALE_NODE_IP>:2283` (If profile enabled, access via IP/port initially)
|
|
||||||
|
|
||||||
**Note:**
|
Replace `<TAILSCALE_NODE>` with your Tailscale device name (e.g., `tailscale-nas.your-tailnet.ts.net`) or its Tailscale IP address.
|
||||||
* `<TAILSCALE_NODE>` refers to the full Tailscale name (e.g., `tailscale-nas.your-tailnet.ts.net`).
|
|
||||||
* `<TAILSCALE_NODE_IP>` refers to the Tailscale IP address of the NAS.
|
If you configure DNS for your `APP_HOSTNAME` variable to point to the Tailscale IP, you can use `https://<APP_HOSTNAME>/<service_path>`.
|
||||||
* Some services (AdGuard, HA, Immich) might require initial setup via their direct IP and port before Tailscale/Traefik routing is fully effective or configured within the application. Authentication for most services will be handled by Authelia (configured later).
|
|
||||||
|
### Setup Script Commands (`update-setup.sh`)
|
||||||
|
|
||||||
|
The `update-setup.sh` script provides various commands to manage your configuration. Run `./update-setup.sh help` to see all options.
|
||||||
|
|
||||||
|
**Core Setup & Updates:**
|
||||||
|
|
||||||
|
* `./update-setup.sh update-env`: Updates `.env` from `.env.example`, preserving existing values and highlighting new/deprecated keys.
|
||||||
|
* `./update-setup.sh update-authelia`: Updates `authelia/configuration.yml` from the example, applying domain settings from `.env` and attempting to preserve secrets (uses `yq` if available).
|
||||||
|
* `./update-setup.sh update-services`: Updates configurations for running *arr/qBittorrent/Bazarr containers (sets URL base, extracts API keys to `.env`). Restarts affected containers.
|
||||||
|
* `./update-setup.sh all`: Runs `update-env`, `update-authelia`, and `update-services` sequentially. Recommended for initial setup and major updates.
|
||||||
|
|
||||||
|
**Authelia Policy Management:**
|
||||||
|
|
||||||
|
* `./update-setup.sh manage-policies`: Starts an interactive menu to list or set Authelia access policies (`one_factor`, `two_factor`, `bypass`, `deny`) for specific services defined in `authelia/configuration.yml`.
|
||||||
|
* `./update-setup.sh list-policies`: Lists services defined in `authelia/configuration.yml` and their current access policy.
|
||||||
|
* `./update-setup.sh set-policy <service> <policy>`: Directly sets the Authelia access policy for the specified `<service>` to the given `<policy>` (e.g., `one_factor`, `two_factor`, `bypass`, `deny`).
|
||||||
|
|
||||||
|
> **Important:** After changing Authelia policies using `manage-policies` or `set-policy`, you **must** restart Authelia for the changes to take effect:
|
||||||
|
> ```bash
|
||||||
|
> docker compose restart authelia
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**User & File Management:**
|
||||||
|
|
||||||
|
* `./update-setup.sh manage-accounts`: Starts an interactive tool to add or update users in `authelia/users_database.yml`. It generates password hashes and prompts for user details.
|
||||||
|
* `./update-setup.sh cleanup`: Interactively finds and deletes old backup files (`.bak`) created by the script. Allows keeping the most recent backup of each type.
|
||||||
|
|
||||||
|
**Help:**
|
||||||
|
|
||||||
|
* `./update-setup.sh help`: Displays the full list of commands and usage instructions.
|
||||||
|
|
||||||
|
### Managing Service Authentication (Authelia Policies)
|
||||||
|
|
||||||
|
Use the `update-setup.sh` script to easily control which services require Authelia login and what level of authentication is needed. This is done by managing *access control rules* within Authelia's configuration (`authelia/configuration.yml`).
|
||||||
|
|
||||||
|
See the `Authelia Policy Management` commands in the [Setup Script Commands](#setup-script-commands-update-setupsh) section above for details on how to list and set policies like `one_factor`, `two_factor`, `bypass`, or `deny` for each service.
|
||||||
|
|
||||||
## Optional Services
|
## Optional Services
|
||||||
|
|
||||||
@@ -250,6 +437,20 @@ Available Profiles:
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Middleware Not Found Errors
|
||||||
|
|
||||||
|
If you see error messages like `middleware "authelia-auth@docker" does not exist` in the Traefik logs, it could be due to one of these issues:
|
||||||
|
|
||||||
|
1. **Docker Network Issue:** The Traefik configuration has been updated to fix network discovery issues when running in Tailscale's network namespace. If you're still seeing this error, try restarting the stack with:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Authentication Variable Missing:** Ensure you have properly configured the `AUTH_*` variables in your `.env` file for the services you want to control. If not specified, most services default to requiring authentication.
|
||||||
|
|
||||||
|
3. **Docker Socket Permissions:** Make sure Traefik can access the Docker socket. See the [SELinux Socket Permissions](#selinux-socket-permissions-docker) section below for more details.
|
||||||
|
|
||||||
### SELinux Socket Permissions (Docker)
|
### SELinux Socket Permissions (Docker)
|
||||||
|
|
||||||
If you are running Docker on a host with SELinux enabled (like Fedora, CentOS, RHEL) and services like Traefik, Watchtower, or Autoheal fail with "permission denied" errors when trying to access `/var/run/docker.sock`:
|
If you are running Docker on a host with SELinux enabled (like Fedora, CentOS, RHEL) and services like Traefik, Watchtower, or Autoheal fail with "permission denied" errors when trying to access `/var/run/docker.sock`:
|
||||||
@@ -270,6 +471,45 @@ If you are running Docker on a host with SELinux enabled (like Fedora, CentOS, R
|
|||||||
```
|
```
|
||||||
This allows the specific actions that were being denied. You might need to repeat this if different denials appear after applying the first policy.
|
This allows the specific actions that were being denied. You might need to repeat this if different denials appear after applying the first policy.
|
||||||
|
|
||||||
|
### Authelia v4.38+ Configuration
|
||||||
|
|
||||||
|
Authelia v4.38+ introduces significant changes to its configuration structure, particularly for session domains and authentication flows. The setup in this repository has been carefully configured to work with these changes:
|
||||||
|
|
||||||
|
1. **Domain Configuration**:
|
||||||
|
- You must use your specific Tailnet domain (e.g., `example.ts.net`) for cookies, not just `ts.net`
|
||||||
|
- The domain `ts.net` is part of the [Public Suffix List](https://publicsuffix.org/), which means browsers restrict cookies on it for security reasons
|
||||||
|
- Authelia will refuse to start if you try to use a domain from this list
|
||||||
|
|
||||||
|
2. **Required Secret Variables**: You must set these four variables in your `.env` file:
|
||||||
|
- `AUTHELIA_JWT_SECRET`: Used for password reset tokens
|
||||||
|
- `AUTHELIA_SESSION_SECRET`: Used for session cookie encryption
|
||||||
|
- `AUTHELIA_STORAGE_ENCRYPTION_KEY`: Used for database encryption
|
||||||
|
- `AUTHELIA_REDIS_PASSWORD`: Used for Redis authentication
|
||||||
|
|
||||||
|
Generate strong random values for these with: `openssl rand -hex 32`
|
||||||
|
|
||||||
|
3. **Automatic Domain Setup**: The `update-setup.sh` script automatically:
|
||||||
|
- Uses your specific Tailnet domain (e.g., `example.ts.net`) from your `.env` file
|
||||||
|
- Configures cookie domains properly to avoid Public Suffix List issues
|
||||||
|
- Sets up proper access control rules for both your domain and its subdomains
|
||||||
|
|
||||||
|
4. **File Permissions**: The Authelia container runs with your user ID and group ID, preventing permission issues when managing the configuration files with git or other tools.
|
||||||
|
|
||||||
|
If you encounter any of these common errors, running the setup script should resolve them:
|
||||||
|
```
|
||||||
|
error: option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list
|
||||||
|
error: option 'authelia_url' does not share a cookie scope with domain
|
||||||
|
error: can't be specified at the same time: option 'domain' and option 'cookies'
|
||||||
|
configuration key 'jwt_secret' is deprecated in 4.38.0
|
||||||
|
```
|
||||||
|
|
||||||
|
After making changes to the configuration, restart Authelia with:
|
||||||
|
```bash
|
||||||
|
docker compose restart authelia
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Authelia documentation](https://www.authelia.com/configuration/session/introduction/) for more details on the v4.38+ configuration structure.
|
||||||
|
|
||||||
### Tailscale Issues
|
### Tailscale Issues
|
||||||
|
|
||||||
* **Authentication:** Ensure your `TAILSCALE_AUTHKEY` in `.env` is valid and hasn't expired (especially if using ephemeral keys). Check the `tailscale` container logs (`docker compose logs tailscale`) for authentication errors.
|
* **Authentication:** Ensure your `TAILSCALE_AUTHKEY` in `.env` is valid and hasn't expired (especially if using ephemeral keys). Check the `tailscale` container logs (`docker compose logs tailscale`) for authentication errors.
|
||||||
|
|||||||
140
authelia/configuration.example.yml
Normal file
140
authelia/configuration.example.yml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Authelia Configuration File v4.38+
|
||||||
|
# Documentation: https://www.authelia.com/configuration/
|
||||||
|
|
||||||
|
# Server settings
|
||||||
|
server:
|
||||||
|
address: 'tcp://0.0.0.0:9091'
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: text
|
||||||
|
|
||||||
|
# Session configuration for v4.38+
|
||||||
|
session:
|
||||||
|
name: authelia_session
|
||||||
|
secret: ${AUTHELIA_SESSION_SECRET}
|
||||||
|
expiration: 1h
|
||||||
|
inactivity: 5m
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
password: ${AUTHELIA_SESSION_REDIS_PASSWORD}
|
||||||
|
database_index: 0
|
||||||
|
cookies:
|
||||||
|
# Using your specific Tailscale domain (e.g. example.ts.net) not just ts.net
|
||||||
|
- domain: 'your-tailnet.ts.net'
|
||||||
|
authelia_url: 'https://tailscale-nas.your-tailnet.ts.net'
|
||||||
|
default_redirection_url: 'https://tailscale-nas.your-tailnet.ts.net/home'
|
||||||
|
same_site: lax
|
||||||
|
|
||||||
|
# Regulation (brute force protection)
|
||||||
|
regulation:
|
||||||
|
max_retries: 3
|
||||||
|
find_time: 2m
|
||||||
|
ban_time: 5m
|
||||||
|
|
||||||
|
# Storage (for user preferences, etc. - encrypted using storage key)
|
||||||
|
storage:
|
||||||
|
encryption_key: ${AUTHELIA_STORAGE_ENCRYPTION_KEY}
|
||||||
|
local:
|
||||||
|
path: /config/db.sqlite3
|
||||||
|
|
||||||
|
# Authentication backend (using file-based user database)
|
||||||
|
authentication_backend:
|
||||||
|
file:
|
||||||
|
path: /config/users_database.yml
|
||||||
|
password:
|
||||||
|
algorithm: argon2id
|
||||||
|
iterations: 1
|
||||||
|
memory: 1024
|
||||||
|
parallelism: 8
|
||||||
|
salt_length: 16
|
||||||
|
key_length: 32
|
||||||
|
|
||||||
|
# Access control rules
|
||||||
|
access_control:
|
||||||
|
default_policy: deny # Deny access by default
|
||||||
|
rules:
|
||||||
|
# Rules are processed in order. First match wins.
|
||||||
|
# It's recommended to put more specific rules first.
|
||||||
|
|
||||||
|
# 1. Bypass rules (No authentication required)
|
||||||
|
# Allow access to Authelia's own endpoints
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/auth.*' # Match /auth and anything after it
|
||||||
|
policy: bypass
|
||||||
|
# Allow access to the root path (will be redirected by Traefik later)
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path: '/'
|
||||||
|
policy: bypass
|
||||||
|
# Allow access to API endpoints (as requested, review security implications)
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/api.*' # Match /api and anything after it
|
||||||
|
policy: bypass
|
||||||
|
|
||||||
|
# 2. One-Factor Authentication Rules (Requires login)
|
||||||
|
# Add rules for each service you want to protect.
|
||||||
|
# The domain should match your Tailscale domain.
|
||||||
|
# The path should match the Traefik PathPrefix for the service.
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/sonarr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/radarr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/lidarr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/bazarr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/qbittorrent.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/sabnzbd.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/calibre.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/home.*' # Protect the homepage
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/jellyseerr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/prowlarr.*'
|
||||||
|
policy: one_factor
|
||||||
|
- domain: '*.your-tailnet.ts.net'
|
||||||
|
path_regex: '^/flaresolverr.*'
|
||||||
|
policy: one_factor
|
||||||
|
# Add other services here following the pattern:
|
||||||
|
# - domain: '*.your-tailnet.ts.net'
|
||||||
|
# path_regex: '^/<service_path>.*'
|
||||||
|
# policy: one_factor
|
||||||
|
|
||||||
|
# 3. Default rule for the domain (optional, if you want a catch-all)
|
||||||
|
# This rule will apply if no path-specific rule above matches.
|
||||||
|
# You might want to deny or require one_factor for unmatched paths.
|
||||||
|
# Example: Deny any other path on the domain
|
||||||
|
# - domain: '*.your-tailnet.ts.net'
|
||||||
|
# policy: deny
|
||||||
|
# Example: Require login for any other path
|
||||||
|
# - domain: '*.your-tailnet.ts.net'
|
||||||
|
# policy: one_factor
|
||||||
|
|
||||||
|
# Notifier configuration
|
||||||
|
notifier:
|
||||||
|
filesystem:
|
||||||
|
filename: /config/notification.txt
|
||||||
|
|
||||||
|
# Identity Validation (includes JWT secret for password reset)
|
||||||
|
identity_validation:
|
||||||
|
reset_password:
|
||||||
|
jwt_secret: ${AUTHELIA_JWT_SECRET}
|
||||||
|
|
||||||
|
# Identity Providers
|
||||||
|
identity_providers:
|
||||||
|
oidc: null
|
||||||
39
authelia/users_database.example.yml
Normal file
39
authelia/users_database.example.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Authelia User Database
|
||||||
|
# Documentation: https://www.authelia.com/configuration/security/authentication/file/
|
||||||
|
|
||||||
|
# To add users:
|
||||||
|
# 1. Generate a password hash:
|
||||||
|
# docker run authelia/authelia:latest authelia hash-password 'your_strong_password'
|
||||||
|
# 2. Add the user entry below.
|
||||||
|
#
|
||||||
|
# To approve registered users (if registration is enabled in configuration.yml):
|
||||||
|
# 1. New users will appear here, possibly commented out or with 'disabled: true'.
|
||||||
|
# 2. Uncomment the user or set 'disabled: false' to grant access.
|
||||||
|
|
||||||
|
users:
|
||||||
|
# First user is typically considered the admin in access rules
|
||||||
|
admin:
|
||||||
|
displayname: "Admin User"
|
||||||
|
# Replace this hash with one generated for your desired password!
|
||||||
|
password: "$argon2id$v=19$m=102400,t=1,p=8$PBf/L9l3s7LwN6jX/B3tVg$9+q3kL8VAbpWj9Gv9Z6uA5bA4zT1fB2fH3aD5c6b7e8" # Example hash for 'password'
|
||||||
|
email: admin@example.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- users
|
||||||
|
|
||||||
|
# Example of a regular user
|
||||||
|
# user1:
|
||||||
|
# displayname: "Regular User"
|
||||||
|
# password: "..." # Generate hash
|
||||||
|
# email: user1@example.com
|
||||||
|
# groups:
|
||||||
|
# - users
|
||||||
|
|
||||||
|
# Example of a registered user waiting for approval (if registration enabled)
|
||||||
|
# newuser:
|
||||||
|
# disabled: true
|
||||||
|
# displayname: "New User"
|
||||||
|
# password: "..." # Hash generated during registration
|
||||||
|
# email: newuser@example.com
|
||||||
|
# groups:
|
||||||
|
# - users
|
||||||
@@ -13,18 +13,57 @@ services:
|
|||||||
- --experimental.plugins.rewrite-body.version=v1.2.0
|
- --experimental.plugins.rewrite-body.version=v1.2.0
|
||||||
- --experimental.plugins.rewriteHeaders.modulename=github.com/XciD/traefik-plugin-rewrite-headers
|
- --experimental.plugins.rewriteHeaders.modulename=github.com/XciD/traefik-plugin-rewrite-headers
|
||||||
- --experimental.plugins.rewriteHeaders.version=v0.0.3
|
- --experimental.plugins.rewriteHeaders.version=v0.0.3
|
||||||
network_mode: service:tailscale # Add this line
|
- --providers.docker.network=docker-compose-nas
|
||||||
# ports: # Remove this section
|
- --providers.docker.endpoint=unix:///var/run/docker.sock
|
||||||
# - "80:80"
|
network_mode: service:tailscale
|
||||||
# - "443:443"
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
# extra_hosts: # Remove this section
|
|
||||||
# - host.docker.internal:172.17.0.1
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: redis
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- REDIS_PASSWORD=${AUTHELIA_REDIS_PASSWORD}
|
||||||
|
command: ["redis-server", "--requirepass", "${AUTHELIA_REDIS_PASSWORD}"]
|
||||||
|
volumes:
|
||||||
|
- ${CONFIG_ROOT:-.}/redis:/data:Z
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "${AUTHELIA_REDIS_PASSWORD}", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
authelia:
|
||||||
|
image: authelia/authelia:latest
|
||||||
|
container_name: authelia
|
||||||
|
restart: always
|
||||||
|
user: ${USER_ID}:${GROUP_ID}
|
||||||
|
volumes:
|
||||||
|
- ${CONFIG_ROOT:-.}/authelia:/config:Z
|
||||||
|
environment:
|
||||||
|
- AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET}
|
||||||
|
- AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY}
|
||||||
|
- AUTHELIA_SESSION_REDIS_PASSWORD=${AUTHELIA_REDIS_PASSWORD}
|
||||||
|
- AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET=${AUTHELIA_JWT_SECRET}
|
||||||
|
- TZ=${TIMEZONE}
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.authelia.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/auth`) # Changed rule
|
||||||
|
- traefik.http.routers.authelia.entrypoints=web
|
||||||
|
# - traefik.http.routers.authelia.priority=100 # Removed priority
|
||||||
|
- traefik.http.services.authelia.loadbalancer.server.port=9091
|
||||||
|
- traefik.http.middlewares.authelia-auth.forwardAuth.address=http://authelia:9091/api/verify # Simplified forwardAuth address
|
||||||
|
- traefik.http.routers.authelia.middlewares=https-proto@docker
|
||||||
|
- traefik.http.middlewares.authelia-auth.forwardAuth.trustForwardHeader=true
|
||||||
|
- traefik.http.middlewares.authelia-auth.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email
|
||||||
|
- homepage.group=Security
|
||||||
|
- homepage.name=Authelia
|
||||||
|
- homepage.icon=authelia.png
|
||||||
|
- homepage.href=/auth # Updated href
|
||||||
|
- homepage.description=Authentication Portal
|
||||||
sonarr:
|
sonarr:
|
||||||
image: lscr.io/linuxserver/sonarr
|
image: lscr.io/linuxserver/sonarr
|
||||||
container_name: sonarr
|
container_name: sonarr
|
||||||
@@ -42,8 +81,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.sonarr.rule=PathPrefix(`/sonarr`)
|
- traefik.http.routers.sonarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/sonarr`) # Added Host check
|
||||||
- traefik.http.routers.sonarr.entrypoints=web
|
- traefik.http.routers.sonarr.entrypoints=web
|
||||||
|
- traefik.http.routers.sonarr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.sonarr.loadbalancer.server.port=8989
|
- traefik.http.services.sonarr.loadbalancer.server.port=8989
|
||||||
- homepage.group=Media
|
- homepage.group=Media
|
||||||
- homepage.name=Sonarr
|
- homepage.name=Sonarr
|
||||||
@@ -71,8 +111,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.radarr.rule=PathPrefix(`/radarr`)
|
- traefik.http.routers.radarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/radarr`) # Added Host check
|
||||||
- traefik.http.routers.radarr.entrypoints=web
|
- traefik.http.routers.radarr.entrypoints=web
|
||||||
|
- traefik.http.routers.radarr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.radarr.loadbalancer.server.port=7878
|
- traefik.http.services.radarr.loadbalancer.server.port=7878
|
||||||
- homepage.group=Media
|
- homepage.group=Media
|
||||||
- homepage.name=Radarr
|
- homepage.name=Radarr
|
||||||
@@ -100,8 +141,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.lidarr.rule=PathPrefix(`/lidarr`)
|
- traefik.http.routers.lidarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/lidarr`) # Added Host check
|
||||||
- traefik.http.routers.lidarr.entrypoints=web
|
- traefik.http.routers.lidarr.entrypoints=web
|
||||||
|
- traefik.http.routers.lidarr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.lidarr.loadbalancer.server.port=8686
|
- traefik.http.services.lidarr.loadbalancer.server.port=8686
|
||||||
- homepage.group=Media
|
- homepage.group=Media
|
||||||
- homepage.name=Lidarr
|
- homepage.name=Lidarr
|
||||||
@@ -131,8 +173,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.bazarr.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/bazarr`)
|
- traefik.http.routers.bazarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/bazarr`) # Added Host check
|
||||||
- traefik.http.routers.bazarr.entrypoints=web
|
- traefik.http.routers.bazarr.entrypoints=web
|
||||||
|
- traefik.http.routers.bazarr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.bazarr.loadbalancer.server.port=6767
|
- traefik.http.services.bazarr.loadbalancer.server.port=6767
|
||||||
- homepage.group=Download
|
- homepage.group=Download
|
||||||
- homepage.name=Bazarr
|
- homepage.name=Bazarr
|
||||||
@@ -165,10 +208,10 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.jellyseerr.rule=PathPrefix(`/jellyseerr`)
|
- traefik.http.routers.jellyseerr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/jellyseerr`) # Added Host check
|
||||||
- traefik.http.routers.jellyseerr.entrypoints=web
|
- traefik.http.routers.jellyseerr.entrypoints=web
|
||||||
- traefik.http.services.jellyseerr.loadbalancer.server.port=5055
|
- traefik.http.services.jellyseerr.loadbalancer.server.port=5055
|
||||||
- traefik.http.routers.jellyseerr.middlewares=jellyseerr-stripprefix,jellyseerr-rewrite,jellyseerr-rewriteHeaders
|
- traefik.http.routers.jellyseerr.middlewares=https-proto@docker,jellyseerr-stripprefix,jellyseerr-rewrite,jellyseerr-rewriteHeaders,authelia-auth@docker
|
||||||
- traefik.http.middlewares.jellyseerr-stripprefix.stripPrefix.prefixes=/jellyseerr
|
- traefik.http.middlewares.jellyseerr-stripprefix.stripPrefix.prefixes=/jellyseerr
|
||||||
- traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].header=location
|
- traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].header=location
|
||||||
- traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].regex=^/(.+)$
|
- traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].regex=^/(.+)$
|
||||||
@@ -239,8 +282,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.prowlarr.rule=PathPrefix(`/prowlarr`)
|
- traefik.http.routers.prowlarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/prowlarr`) # Added Host check
|
||||||
- traefik.http.routers.prowlarr.entrypoints=web
|
- traefik.http.routers.prowlarr.entrypoints=web
|
||||||
|
- traefik.http.routers.prowlarr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.prowlarr.loadbalancer.server.port=9696
|
- traefik.http.services.prowlarr.loadbalancer.server.port=9696
|
||||||
- homepage.group=Download
|
- homepage.group=Download
|
||||||
- homepage.name=Prowlarr
|
- homepage.name=Prowlarr
|
||||||
@@ -262,8 +306,9 @@ services:
|
|||||||
- TZ=${TIMEZONE}
|
- TZ=${TIMEZONE}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.flaresolverr.rule=PathPrefix(`/flaresolverr`)
|
- traefik.http.routers.flaresolverr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/flaresolverr`) # Added Host check
|
||||||
- traefik.http.routers.flaresolverr.entrypoints=web
|
- traefik.http.routers.flaresolverr.entrypoints=web
|
||||||
|
- traefik.http.routers.flaresolverr.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.flaresolverr.loadbalancer.server.port=8191
|
- traefik.http.services.flaresolverr.loadbalancer.server.port=8191
|
||||||
profiles:
|
profiles:
|
||||||
- flaresolverr
|
- flaresolverr
|
||||||
@@ -281,25 +326,20 @@ services:
|
|||||||
- ${DOWNLOAD_ROOT}:/data/torrents:Z
|
- ${DOWNLOAD_ROOT}:/data/torrents:Z
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# Container may fail if the PIA's token expired, so mark as unhealthy when there is no internet connection
|
|
||||||
# see: https://github.com/qdm12/gluetun/issues/641#issuecomment-933856220
|
|
||||||
test:
|
test:
|
||||||
["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
|
["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.qbittorrent.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/qbittorrent`)
|
- traefik.http.routers.qbittorrent.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/qbittorrent`) # Added Host check
|
||||||
- traefik.http.routers.qbittorrent.entrypoints=web
|
- traefik.http.routers.qbittorrent.entrypoints=web
|
||||||
- traefik.http.services.qbittorrent.loadbalancer.server.port=8080
|
- traefik.http.services.qbittorrent.loadbalancer.server.port=8080
|
||||||
- traefik.http.routers.qbittorrent.middlewares=qbittorrent-strip-slash,qbittorrent-stripprefix
|
- traefik.http.routers.qbittorrent.middlewares=https-proto@docker,qbittorrent-strip-slash,qbittorrent-stripprefix,authelia-auth@docker
|
||||||
# https://github.com/qbittorrent/qBittorrent/issues/5693#issuecomment-552146296
|
|
||||||
- traefik.http.middlewares.qbittorrent-stripprefix.stripPrefix.prefixes=/qbittorrent
|
- traefik.http.middlewares.qbittorrent-stripprefix.stripPrefix.prefixes=/qbittorrent
|
||||||
# https://community.traefik.io/t/middleware-to-add-the-if-needed/1895/19
|
|
||||||
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.regex=(^.*\/qbittorrent$$)
|
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.regex=(^.*\/qbittorrent$$)
|
||||||
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.replacement=$$1/
|
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.replacement=$$1/
|
||||||
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.permanent=false
|
- traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.permanent=false
|
||||||
#- com.centurylinklabs.watchtower.depends-on=/vpn
|
|
||||||
- homepage.group=Download
|
- homepage.group=Download
|
||||||
- homepage.name=qBittorrent
|
- homepage.name=qBittorrent
|
||||||
- homepage.icon=qbittorrent.png
|
- homepage.icon=qbittorrent.png
|
||||||
@@ -338,8 +378,9 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.sabnzbd.rule=PathPrefix(`/sabnzbd`) # Simplified rule
|
- traefik.http.routers.sabnzbd.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/sabnzbd`) # Added Host check
|
||||||
- traefik.http.routers.sabnzbd.entrypoints=web
|
- traefik.http.routers.sabnzbd.entrypoints=web
|
||||||
|
- traefik.http.routers.sabnzbd.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
- traefik.http.services.sabnzbd.loadbalancer.server.port=8080
|
- traefik.http.services.sabnzbd.loadbalancer.server.port=8080
|
||||||
- homepage.group=Download
|
- homepage.group=Download
|
||||||
- homepage.name=Sabnzbd
|
- homepage.name=Sabnzbd
|
||||||
@@ -359,7 +400,7 @@ services:
|
|||||||
- PUID=${USER_ID}
|
- PUID=${USER_ID}
|
||||||
- PGID=${GROUP_ID}
|
- PGID=${GROUP_ID}
|
||||||
- TZ=${TIMEZONE}
|
- TZ=${TIMEZONE}
|
||||||
- JELLYFIN_PublishedServerUrl=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/jellyfin
|
- JELLYFIN_PublishedServerUrl=https://${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/jellyfin
|
||||||
volumes:
|
volumes:
|
||||||
- ${CONFIG_ROOT:-.}/jellyfin:/config:Z
|
- ${CONFIG_ROOT:-.}/jellyfin:/config:Z
|
||||||
- ${DATA_ROOT}:/data:Z
|
- ${DATA_ROOT}:/data:Z
|
||||||
@@ -373,8 +414,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.jellyfin.rule=PathPrefix(`/jellyfin`)
|
- traefik.http.routers.jellyfin.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/jellyfin`) # Added Host check
|
||||||
- traefik.http.routers.jellyfin.entrypoints=web
|
- traefik.http.routers.jellyfin.entrypoints=web
|
||||||
|
- traefik.http.routers.jellyfin.middlewares=https-proto@docker # Only HTTPS, no auth
|
||||||
- traefik.http.services.jellyfin.loadbalancer.server.port=8096
|
- traefik.http.services.jellyfin.loadbalancer.server.port=8096
|
||||||
- homepage.group=Media
|
- homepage.group=Media
|
||||||
- homepage.name=Jellyfin
|
- homepage.name=Jellyfin
|
||||||
@@ -403,8 +445,8 @@ services:
|
|||||||
- traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Scheme=https
|
- traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Scheme=https
|
||||||
- traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Script-Name=/calibre
|
- traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Script-Name=/calibre
|
||||||
- traefik.http.middlewares.calibre-stripprefixregex.stripPrefixRegex.regex=/calibre
|
- traefik.http.middlewares.calibre-stripprefixregex.stripPrefixRegex.regex=/calibre
|
||||||
- traefik.http.routers.calibre.middlewares=calibre-headers,calibre-stripprefixregex
|
- traefik.http.routers.calibre.middlewares=https-proto@docker,calibre-headers,calibre-stripprefixregex,authelia-auth@docker
|
||||||
- traefik.http.routers.calibre.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/calibre`)
|
- traefik.http.routers.calibre.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/calibre`) # Added Host check
|
||||||
- traefik.http.routers.calibre.entrypoints=web
|
- traefik.http.routers.calibre.entrypoints=web
|
||||||
- traefik.http.services.calibre.loadbalancer.server.port=8083
|
- traefik.http.services.calibre.loadbalancer.server.port=8083
|
||||||
- homepage.group=Media
|
- homepage.group=Media
|
||||||
@@ -467,7 +509,6 @@ services:
|
|||||||
- HOMEPAGE_VAR_WEATHER_LONG=${HOMEPAGE_VAR_WEATHER_LONG}
|
- HOMEPAGE_VAR_WEATHER_LONG=${HOMEPAGE_VAR_WEATHER_LONG}
|
||||||
- HOMEPAGE_VAR_WEATHER_TIME=${TIMEZONE}
|
- HOMEPAGE_VAR_WEATHER_TIME=${TIMEZONE}
|
||||||
- HOMEPAGE_VAR_WEATHER_UNIT=${HOMEPAGE_VAR_WEATHER_UNIT}
|
- HOMEPAGE_VAR_WEATHER_UNIT=${HOMEPAGE_VAR_WEATHER_UNIT}
|
||||||
# Explicitly allow the hostname constructed from Tailscale variables
|
|
||||||
- HOMEPAGE_ALLOWED_HOSTS=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}
|
- HOMEPAGE_ALLOWED_HOSTS=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}
|
||||||
volumes:
|
volumes:
|
||||||
- ${CONFIG_ROOT:-.}/homepage:/app/config:Z
|
- ${CONFIG_ROOT:-.}/homepage:/app/config:Z
|
||||||
@@ -478,11 +519,17 @@ services:
|
|||||||
[sh, -c, "cp -n /app/config/tpl/*.yaml /app/config && node server.js"]
|
[sh, -c, "cp -n /app/config/tpl/*.yaml /app/config && node server.js"]
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
# Change path to /home and use specific Tailscale host
|
- traefik.http.routers.homepage.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/`) # Changed rule to root
|
||||||
- traefik.http.routers.homepage.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/home`)
|
|
||||||
- traefik.http.routers.homepage.entrypoints=web
|
- traefik.http.routers.homepage.entrypoints=web
|
||||||
# Authelia middleware will be added in a later commit
|
# - traefik.http.routers.homepage.priority=10 # Removed priority
|
||||||
- traefik.http.services.homepage.loadbalancer.server.port=3000
|
# Global middleware for setting HTTPS header
|
||||||
|
- traefik.http.middlewares.https-proto.headers.customrequestheaders.X-Forwarded-Proto=https
|
||||||
|
- traefik.http.routers.homepage.middlewares=https-proto@docker,authelia-auth@docker
|
||||||
|
- homepage.group=Dashboard
|
||||||
|
- homepage.name=Homepage
|
||||||
|
- homepage.icon=homepage.png
|
||||||
|
- homepage.href=/ # Updated href
|
||||||
|
- homepage.description=Service Dashboard
|
||||||
watchtower:
|
watchtower:
|
||||||
image: ghcr.io/containrrr/watchtower:latest
|
image: ghcr.io/containrrr/watchtower:latest
|
||||||
container_name: watchtower
|
container_name: watchtower
|
||||||
@@ -502,23 +549,22 @@ services:
|
|||||||
tailscale:
|
tailscale:
|
||||||
image: tailscale/tailscale:latest
|
image: tailscale/tailscale:latest
|
||||||
container_name: tailscale
|
container_name: tailscale
|
||||||
hostname: ${TAILSCALE_HOSTNAME:-tailscale-nas} # Hostname for Tailscale access
|
hostname: ${TAILSCALE_HOSTNAME:-tailscale-nas}
|
||||||
environment:
|
environment:
|
||||||
TS_AUTHKEY: ${TAILSCALE_AUTHKEY} # Needs to be set in .env
|
TS_AUTHKEY: ${TAILSCALE_AUTHKEY}
|
||||||
TS_EXTRA_ARGS: "--advertise-tags=${TAILSCALE_TAGS:-tag:nas}" # Keep tags if desired
|
TS_EXTRA_ARGS: "--advertise-tags=${TAILSCALE_TAGS:-tag:nas}"
|
||||||
TS_STATE_DIR: "/var/lib/tailscale"
|
TS_STATE_DIR: "/var/lib/tailscale"
|
||||||
TS_USERSPACE: "false"
|
TS_USERSPACE: "false"
|
||||||
# Switch to enable Funnel (public access) or Serve (Tailnet only)
|
|
||||||
ENABLE_FUNNEL_HTTPS: ${ENABLE_FUNNEL_HTTPS:-false}
|
ENABLE_FUNNEL_HTTPS: ${ENABLE_FUNNEL_HTTPS:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- ${CONFIG_ROOT:-.}/tailscale/state:/var/lib/tailscale:Z # Persist state
|
- ${CONFIG_ROOT:-.}/tailscale/state:/var/lib/tailscale:Z
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Optional, keep if needed
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
devices:
|
devices:
|
||||||
- /dev/net/tun:/dev/net/tun
|
- /dev/net/tun:/dev/net/tun
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
extra_hosts: # Add this section
|
extra_hosts:
|
||||||
- host.docker.internal:172.17.0.1
|
- host.docker.internal:172.17.0.1
|
||||||
restart: always
|
restart: always
|
||||||
command:
|
command:
|
||||||
@@ -542,8 +588,6 @@ services:
|
|||||||
done
|
done
|
||||||
echo " Tailscaled is running."
|
echo " Tailscaled is running."
|
||||||
|
|
||||||
# --- Start Tailscale Funnel/Serve ---
|
|
||||||
# Check the ENABLE_FUNNEL_HTTPS variable
|
|
||||||
if [ "${ENABLE_FUNNEL_HTTPS}" = "true" ]; then
|
if [ "${ENABLE_FUNNEL_HTTPS}" = "true" ]; then
|
||||||
echo "ENABLE_FUNNEL_HTTPS is true. Setting up Funnel -> http://localhost:80..."
|
echo "ENABLE_FUNNEL_HTTPS is true. Setting up Funnel -> http://localhost:80..."
|
||||||
tailscale funnel --bg http://localhost:80
|
tailscale funnel --bg http://localhost:80
|
||||||
@@ -553,10 +597,9 @@ services:
|
|||||||
tailscale serve --bg http://localhost:80
|
tailscale serve --bg http://localhost:80
|
||||||
echo "Tailscale Serve configured."
|
echo "Tailscale Serve configured."
|
||||||
fi
|
fi
|
||||||
# --- End Tailscale Funnel/Serve ---
|
|
||||||
|
|
||||||
echo "Tailscale forwarding configured. Container will remain running."
|
echo "Tailscale forwarding configured. Container will remain running."
|
||||||
wait # Wait indefinitely for background processes
|
wait
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# See https://stackoverflow.com/a/44864004 for the sed GNU/BSD compatible hack
|
|
||||||
|
|
||||||
function update_arr_config {
|
|
||||||
echo "Updating ${container} configuration..."
|
|
||||||
until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done
|
|
||||||
sed -i.bak "s/<UrlBase><\/UrlBase>/<UrlBase>\/$1<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak
|
|
||||||
CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]')
|
|
||||||
sed -i.bak 's/^'"${CONTAINER_NAME_UPPER}"'_API_KEY=.*/'"${CONTAINER_NAME_UPPER}"'_API_KEY='"$(sed -n 's/.*<ApiKey>\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak
|
|
||||||
echo "Update of ${container} configuration complete, restarting..."
|
|
||||||
docker compose restart "$container"
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_qbittorrent_config {
|
|
||||||
echo "Updating ${container} configuration..."
|
|
||||||
docker compose stop "$container"
|
|
||||||
until [ -f "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf ]; do sleep 1; done
|
|
||||||
sed -i.bak '/WebUI\\ServerDomains=*/a WebUI\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf && rm "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf.bak
|
|
||||||
echo "Update of ${container} configuration complete, restarting..."
|
|
||||||
docker compose start "$container"
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_bazarr_config {
|
|
||||||
echo "Updating ${container} configuration..."
|
|
||||||
until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done
|
|
||||||
sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak
|
|
||||||
sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak
|
|
||||||
sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak
|
|
||||||
until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done
|
|
||||||
SONARR_API_KEY=$(sed -n 's/.*<ApiKey>\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml)
|
|
||||||
sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak
|
|
||||||
until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done
|
|
||||||
RADARR_API_KEY=$(sed -n 's/.*<ApiKey>\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml)
|
|
||||||
sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak
|
|
||||||
sed -i.bak 's/^BAZARR_API_KEY=.*/BAZARR_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak
|
|
||||||
echo "Update of ${container} configuration complete, restarting..."
|
|
||||||
docker compose restart "$container"
|
|
||||||
}
|
|
||||||
|
|
||||||
for container in $(docker ps --format '{{.Names}}'); do
|
|
||||||
if [[ "$container" =~ ^(radarr|sonarr|lidarr|prowlarr)$ ]]; then
|
|
||||||
update_arr_config "$container"
|
|
||||||
elif [[ "$container" =~ ^(bazarr)$ ]]; then
|
|
||||||
update_bazarr_config "$container"
|
|
||||||
elif [[ "$container" =~ ^(qbittorrent)$ ]]; then
|
|
||||||
update_qbittorrent_config "$container"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
936
update-setup.sh
Executable file
936
update-setup.sh
Executable file
@@ -0,0 +1,936 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Combined update script for docker-compose-nas
|
||||||
|
# - Updates .env file from .env.example while preserving values
|
||||||
|
# - Updates Authelia configuration from example file
|
||||||
|
# - Configures services with correct paths and API keys
|
||||||
|
# - Manages Authelia accounts
|
||||||
|
# - Controls service authentication requirements
|
||||||
|
# Created: April 26, 2025
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Color definitions
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Files
|
||||||
|
ENV_FILE=".env"
|
||||||
|
ENV_EXAMPLE=".env.example"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
||||||
|
ENV_BACKUP=".env.${TIMESTAMP}.bak"
|
||||||
|
AUTHELIA_CONFIG="authelia/configuration.yml"
|
||||||
|
AUTHELIA_CONFIG_EXAMPLE="authelia/configuration.example.yml"
|
||||||
|
AUTHELIA_CONFIG_BACKUP="authelia/configuration.${TIMESTAMP}.bak"
|
||||||
|
COMPOSE_FILE="docker-compose.yml"
|
||||||
|
COMPOSE_BACKUP="docker-compose.${TIMESTAMP}.bak"
|
||||||
|
|
||||||
|
# Check if yq is installed
|
||||||
|
check_yq() {
|
||||||
|
if ! command -v yq &> /dev/null; then
|
||||||
|
echo -e "${YELLOW}Warning: 'yq' is not installed. While not required, it provides better YAML handling.${NC}"
|
||||||
|
echo -e "${YELLOW}Installation instructions: https://github.com/mikefarah/yq#install${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print section header
|
||||||
|
print_header() {
|
||||||
|
echo -e "\n${CYAN}${BOLD}$1${NC}"
|
||||||
|
echo -e "${CYAN}$(printf '=%.0s' $(seq 1 ${#1}))${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a file exists
|
||||||
|
check_file() {
|
||||||
|
if [ ! -f "$1" ]; then
|
||||||
|
echo -e "${RED}Error: $1 doesn't exist${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create a backup
|
||||||
|
create_backup() {
|
||||||
|
echo -e "${BLUE}Creating backup of $1 as $2...${NC}"
|
||||||
|
cp "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# PART 1: Update .env file from .env.example
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
update_env_file() {
|
||||||
|
print_header "Environment File Update Tool"
|
||||||
|
|
||||||
|
# Check if files exist
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo -e "${RED}Error: $ENV_FILE doesn't exist${NC}"
|
||||||
|
echo -e "Creating a new $ENV_FILE from $ENV_EXAMPLE"
|
||||||
|
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||||
|
echo -e "${GREEN}Done! Please review and fill in required values in $ENV_FILE${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! check_file "$ENV_EXAMPLE"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}This will update your $ENV_FILE based on the structure in $ENV_EXAMPLE${NC}"
|
||||||
|
echo -e "${BLUE}Your existing values will be preserved where possible${NC}"
|
||||||
|
echo -e "${BLUE}Backup will be created as: $ENV_BACKUP${NC}"
|
||||||
|
echo -e "${YELLOW}Continue? [y/N]:${NC}"
|
||||||
|
read -r answer
|
||||||
|
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}Environment update cancelled.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create backup of current .env
|
||||||
|
create_backup "$ENV_FILE" "$ENV_BACKUP"
|
||||||
|
|
||||||
|
# Store current env values
|
||||||
|
echo -e "${BLUE}Reading current environment values...${NC}"
|
||||||
|
declare -A current_values
|
||||||
|
declare -A current_keys_present
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
# Skip comments and empty lines
|
||||||
|
if [[ ! "$key" =~ ^#.*$ ]] && [[ ! -z "$key" ]]; then
|
||||||
|
# Clean up any comments after the value
|
||||||
|
value=$(echo "$value" | sed 's/[[:space:]]*#.*$//')
|
||||||
|
# Trim leading/trailing whitespace
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
value=$(echo "$value" | xargs)
|
||||||
|
# Store in associative array if key is not empty
|
||||||
|
if [[ ! -z "$key" ]]; then
|
||||||
|
current_values["$key"]="$value"
|
||||||
|
# Track that this key existed in original file, regardless of value
|
||||||
|
current_keys_present["$key"]=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$ENV_FILE"
|
||||||
|
|
||||||
|
# Create new env file from example
|
||||||
|
echo -e "${BLUE}Creating new $ENV_FILE from $ENV_EXAMPLE...${NC}"
|
||||||
|
cp "$ENV_EXAMPLE" "$ENV_FILE.new"
|
||||||
|
|
||||||
|
# Track which keys from the current env have been used
|
||||||
|
declare -A used_keys
|
||||||
|
# Track new keys that need attention
|
||||||
|
new_keys=()
|
||||||
|
# Track keys with special warnings
|
||||||
|
special_keys=()
|
||||||
|
|
||||||
|
# Process the template and fill in values from current env
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^([A-Za-z0-9_]+)=(.*)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
default_value="${BASH_REMATCH[2]}"
|
||||||
|
|
||||||
|
# Mark the key as used if it exists in the original file
|
||||||
|
if [[ -n "${current_keys_present[$key]}" ]]; then
|
||||||
|
used_keys["$key"]=1
|
||||||
|
|
||||||
|
# Replace the line with the current value if one exists
|
||||||
|
if [[ -n "${current_values[$key]}" ]]; then
|
||||||
|
sed -i "s|^$key=.*$|$key=${current_values[$key]}|" "$ENV_FILE.new"
|
||||||
|
fi
|
||||||
|
# If key doesn't exist in original file and has empty/placeholder value
|
||||||
|
elif [[ -z "$default_value" ]] || [[ "$default_value" == '""' ]] || [[ "$default_value" == "''" ]]; then
|
||||||
|
new_keys+=("$key")
|
||||||
|
|
||||||
|
# Special attention for Authelia keys
|
||||||
|
if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then
|
||||||
|
special_keys+=("$key")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$ENV_FILE.new"
|
||||||
|
|
||||||
|
# Create section for unused/deprecated keys at the bottom of the file
|
||||||
|
echo -e "\n\n# --- DEPRECATED OR UNUSED KEYS (Kept for Reference) ---" >> "$ENV_FILE.new"
|
||||||
|
echo -e "# Keys below were in your original .env but aren't in the current .env.example" >> "$ENV_FILE.new"
|
||||||
|
echo -e "# They may be deprecated or renamed. Review and remove if no longer needed\n" >> "$ENV_FILE.new"
|
||||||
|
|
||||||
|
unused_keys_count=0
|
||||||
|
for key in "${!current_values[@]}"; do
|
||||||
|
if [[ -z "${used_keys[$key]}" ]]; then
|
||||||
|
echo "$key=${current_values[$key]} # DEPRECATED/UNUSED - Review" >> "$ENV_FILE.new"
|
||||||
|
unused_keys_count=$((unused_keys_count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Replace the old file with the new one
|
||||||
|
mv "$ENV_FILE.new" "$ENV_FILE"
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
echo -e "\n${GREEN}${BOLD}Environment Update Complete!${NC}"
|
||||||
|
echo -e "${BLUE}Summary:${NC}"
|
||||||
|
echo -e " - ${CYAN}Original config backed up to: $ENV_BACKUP${NC}"
|
||||||
|
echo -e " - ${CYAN}Updated .env structure to match .env.example${NC}"
|
||||||
|
echo -e " - ${CYAN}Preserved ${#used_keys[@]} existing values${NC}"
|
||||||
|
|
||||||
|
if [[ $unused_keys_count -gt 0 ]]; then
|
||||||
|
echo -e " - ${YELLOW}Found $unused_keys_count deprecated/unused keys${NC}"
|
||||||
|
echo -e " ${YELLOW}These have been moved to the bottom of the file with warnings${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#new_keys[@]} -gt 0 ]]; then
|
||||||
|
echo -e "\n${YELLOW}${BOLD}NEW KEYS NEEDING ATTENTION:${NC}"
|
||||||
|
echo -e "${YELLOW}The following keys are new and may need values set:${NC}"
|
||||||
|
for key in "${new_keys[@]}"; do
|
||||||
|
echo -e " - ${MAGENTA}$key${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#special_keys[@]} -gt 0 ]]; then
|
||||||
|
echo -e "\n${RED}${BOLD}IMPORTANT SECURITY KEYS:${NC}"
|
||||||
|
echo -e "${RED}The following keys require secure values:${NC}"
|
||||||
|
for key in "${special_keys[@]}"; do
|
||||||
|
echo -e " - ${MAGENTA}$key${NC}"
|
||||||
|
|
||||||
|
# Specific advice for Authelia keys
|
||||||
|
if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then
|
||||||
|
echo -e " ${CYAN}Generate with: ${GREEN}openssl rand -hex 32${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Review your updated $ENV_FILE file and adjust any values as needed.${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# PART 2: Update Authelia configuration
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
update_authelia_config() {
|
||||||
|
print_header "Authelia Configuration Update Tool"
|
||||||
|
|
||||||
|
# Check if files exist
|
||||||
|
if ! check_file "$AUTHELIA_CONFIG_EXAMPLE"; then
|
||||||
|
echo -e "${RED}Error: Example configuration file '$AUTHELIA_CONFIG_EXAMPLE' doesn't exist${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! check_file "$ENV_FILE"; then
|
||||||
|
echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot retrieve domain settings.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the tailnet domain and hostname from .env
|
||||||
|
local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'")
|
||||||
|
local TAILSCALE_HOSTNAME=$(grep -oP "^TAILSCALE_HOSTNAME=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'")
|
||||||
|
local FULL_HOSTNAME="${TAILSCALE_HOSTNAME}.${TAILNET_DOMAIN}"
|
||||||
|
local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}"
|
||||||
|
|
||||||
|
if [ -z "$TAILNET_DOMAIN" ] || [ -z "$TAILSCALE_HOSTNAME" ]; then
|
||||||
|
echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN or TAILSCALE_HOSTNAME from $ENV_FILE${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If config file doesn't exist, create it from example
|
||||||
|
if [ ! -f "$AUTHELIA_CONFIG" ]; then
|
||||||
|
echo -e "${YELLOW}Authelia configuration file '$AUTHELIA_CONFIG' doesn't exist, creating from example...${NC}"
|
||||||
|
cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG"
|
||||||
|
echo -e "${GREEN}Created new Authelia configuration file.${NC}"
|
||||||
|
# Proceed to update the newly created file
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}This will update your Authelia configuration '$AUTHELIA_CONFIG' based on '$AUTHELIA_CONFIG_EXAMPLE'${NC}"
|
||||||
|
echo -e "${BLUE}Your Tailscale domain settings from '$ENV_FILE' will be applied.${NC}"
|
||||||
|
echo -e "${BLUE}Backup will be created as: $AUTHELIA_CONFIG_BACKUP${NC}"
|
||||||
|
echo -e "${YELLOW}Continue? [y/N]:${NC}"
|
||||||
|
read -r answer
|
||||||
|
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}Authelia config update cancelled.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Create backup of current config only if it exists
|
||||||
|
create_backup "$AUTHELIA_CONFIG" "$AUTHELIA_CONFIG_BACKUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for yq
|
||||||
|
if check_yq; then
|
||||||
|
echo -e "${BLUE}Using 'yq' to update Authelia configuration...${NC}"
|
||||||
|
# Create a temporary file from the example
|
||||||
|
local TEMP_CONFIG="${AUTHELIA_CONFIG}.tmp"
|
||||||
|
cp "$AUTHELIA_CONFIG_EXAMPLE" "$TEMP_CONFIG"
|
||||||
|
|
||||||
|
# Preserve specific existing values if the original config exists
|
||||||
|
if [ -f "$AUTHELIA_CONFIG_BACKUP" ]; then # Use backup as source of truth for existing values
|
||||||
|
echo -e "${BLUE}Attempting to preserve existing secrets and notifier settings...${NC}"
|
||||||
|
local existing_jwt_secret=$(yq e '.identity_validation.reset_password.jwt_secret // ""' "$AUTHELIA_CONFIG_BACKUP")
|
||||||
|
local existing_session_secret=$(yq e '.session.secret // ""' "$AUTHELIA_CONFIG_BACKUP")
|
||||||
|
local existing_storage_key=$(yq e '.storage.encryption_key // ""' "$AUTHELIA_CONFIG_BACKUP")
|
||||||
|
local existing_redis_pass=$(yq e '.session.redis.password // ""' "$AUTHELIA_CONFIG_BACKUP")
|
||||||
|
local existing_notifier=$(yq e '.notifier // ""' "$AUTHELIA_CONFIG_BACKUP")
|
||||||
|
|
||||||
|
# Update secrets in temp file if they existed in the backup
|
||||||
|
if [[ -n "$existing_jwt_secret" && "$existing_jwt_secret" != '""' && "$existing_jwt_secret" != "null" ]]; then
|
||||||
|
yq e -i '.identity_validation.reset_password.jwt_secret = strenv(existing_jwt_secret)' --env existing_jwt_secret="$existing_jwt_secret" "$TEMP_CONFIG"
|
||||||
|
##################################################
|
||||||
|
# PART 5: Authelia Account Management
|
||||||
|
##################################################
|
||||||
|
# PART 4: Authelia Policy Management
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
# Get the current policy for a service from Authelia config
|
||||||
|
# Usage: get_authelia_policy <service_name> <config_file>
|
||||||
|
# Output: policy (e.g., one_factor, bypass), not_found, or error
|
||||||
|
get_authelia_policy() {
|
||||||
|
local service=$1
|
||||||
|
local config_file=$2
|
||||||
|
|
||||||
|
if ! check_file "$config_file"; then
|
||||||
|
echo "error: config file not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use yq if available for more reliable YAML parsing
|
||||||
|
if check_yq; then
|
||||||
|
# Find the rule matching the path_regex for the service
|
||||||
|
# Note: This assumes path_regex is unique enough for the service (e.g., ^/service.*)
|
||||||
|
local policy=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\") | .policy" "$config_file" 2>/dev/null)
|
||||||
|
if [ -n "$policy" ] && [ "$policy" != "null" ]; then
|
||||||
|
echo "$policy"
|
||||||
|
else
|
||||||
|
# Check if a rule exists for the service path at all
|
||||||
|
local rule_exists=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\")" "$config_file" 2>/dev/null)
|
||||||
|
if [ -n "$rule_exists" ] && [ "$rule_exists" != "null" ]; then
|
||||||
|
echo "error: policy not found in rule" # Rule exists but policy couldn't be read
|
||||||
|
else
|
||||||
|
echo "not_found" # No rule found for this service path
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fall back to grep if yq isn't available (less reliable)
|
||||||
|
# This is fragile and depends heavily on formatting
|
||||||
|
local policy_line=$(grep -A 1 "path_regex: '^/${service}.*'" "$config_file" | grep "policy:")
|
||||||
|
if [ -n "$policy_line" ]; then
|
||||||
|
echo "$policy_line" | awk '{print $2}'
|
||||||
|
else
|
||||||
|
echo "not_found"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the policy for a service in Authelia config
|
||||||
|
# Usage: set_authelia_policy <service_name> <policy> <config_file>
|
||||||
|
set_authelia_policy() {
|
||||||
|
local service=$1
|
||||||
|
local policy=$2
|
||||||
|
local config_file=$3
|
||||||
|
local backup_file="${config_file}.${TIMESTAMP}.bak"
|
||||||
|
|
||||||
|
if ! check_file "$config_file"; then
|
||||||
|
echo -e "${RED}Error: Authelia configuration file '$config_file' not found.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$policy" != "one_factor" && "$policy" != "two_factor" && "$policy" != "deny" && "$policy" != "bypass" ]]; then
|
||||||
|
echo -e "${RED}Error: Invalid policy '$policy'. Must be one of: one_factor, two_factor, deny, bypass.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Setting policy for service '$service' to '$policy' in '$config_file'...${NC}"
|
||||||
|
|
||||||
|
# Create backup if it doesn't exist for this run
|
||||||
|
if [ ! -f "$backup_file" ]; then
|
||||||
|
create_backup "$config_file" "$backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use yq if available
|
||||||
|
if check_yq; then
|
||||||
|
# Check if the rule exists first using the path_regex
|
||||||
|
local rule_index=$(yq e ".access_control.rules | map(.path_regex == \"^/${service}.*\") | indexOf(true)" "$config_file" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$rule_index" == "-1" ] || [ -z "$rule_index" ]; then
|
||||||
|
echo -e "${YELLOW}Warning: No rule found for service path '^/${service}.*' in '$config_file'.${NC}"
|
||||||
|
echo -e "${YELLOW}Attempting to add a new rule...${NC}"
|
||||||
|
|
||||||
|
# Get the tailnet domain from .env for the new rule
|
||||||
|
local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'")
|
||||||
|
if [ -z "$TAILNET_DOMAIN" ]; then
|
||||||
|
echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN from $ENV_FILE. Cannot add rule.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}"
|
||||||
|
|
||||||
|
# Add the new rule to the access_control.rules array
|
||||||
|
# Places it before the generic domain rule if it exists, otherwise at the end
|
||||||
|
# This assumes a generic rule like "- domain: '*.domain.tld'" exists near the end
|
||||||
|
yq e -i ".access_control.rules |= select(.domain != \"${WILDCARD_DOMAIN}\" or .path_regex != null) + [{\"domain\": \"${WILDCARD_DOMAIN}\", \"path_regex\": \"^/${service}.*\", \"policy\": \"${policy}\"}] + select(.domain == \"${WILDCARD_DOMAIN}\" and .path_regex == null)" "$config_file"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}Added new rule for '$service' with policy '$policy'.${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Failed to add new rule for '$service' using yq.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Rule exists, update the policy at the found index
|
||||||
|
yq e -i "(.access_control.rules[$rule_index].policy) = \"$policy\"" "$config_file"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}Policy for '$service' updated to '$policy'.${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Failed to update policy for '$service' using yq.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback to sed (much less reliable, especially for adding rules)
|
||||||
|
echo -e "${YELLOW}Warning: 'yq' not found. Using 'sed' which is less reliable for YAML manipulation.${NC}"
|
||||||
|
# Check if rule exists (simple grep)
|
||||||
|
if grep -q "path_regex: '^/${service}.*'" "$config_file"; then
|
||||||
|
# Attempt to find the line number of path_regex and update the policy line below it
|
||||||
|
local line_num=$(grep -n "path_regex: '^/${service}.*'" "$config_file" | head -n 1 | cut -d: -f1) # Use head -n 1 just in case
|
||||||
|
if [ -n "$line_num" ]; then
|
||||||
|
# Assuming policy is the next line (fragile!)
|
||||||
|
local policy_line_num=$((line_num + 1))
|
||||||
|
# Check if the next line actually contains 'policy:'
|
||||||
|
if sed -n "${policy_line_num}p" "$config_file" | grep -q "policy:"; then
|
||||||
|
sed -i "${policy_line_num}s/policy:.*/policy: $policy/" "$config_file"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}Policy for '$service' updated to '$policy' (using sed).${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Failed to update policy line using sed.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Could not reliably find policy line for '$service' using sed (expected on line $policy_line_num).${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Could not find line number for service '$service' using sed.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Rule for service '$service' not found. Cannot add rule using sed.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# List services and their Authelia policy status
|
||||||
|
list_authelia_services() {
|
||||||
|
print_header "Authelia Service Policy Status"
|
||||||
|
|
||||||
|
if ! check_file "$AUTHELIA_CONFIG"; then
|
||||||
|
echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Checking service policies in $AUTHELIA_CONFIG...${NC}"
|
||||||
|
echo -e "${CYAN}SERVICE\t\tPOLICY${NC}"
|
||||||
|
echo -e "${CYAN}-------\t\t------${NC}"
|
||||||
|
|
||||||
|
local service_count=0
|
||||||
|
local processed_services="" # Track processed services
|
||||||
|
|
||||||
|
# Use yq to get all rules with path_regex if available
|
||||||
|
if check_yq; then
|
||||||
|
# Extract path_regex and policy for rules that have path_regex
|
||||||
|
local rules_data=$(yq e '.access_control.rules[] | select(has("path_regex")) | {"path": .path_regex, "policy": .policy}' "$AUTHELIA_CONFIG")
|
||||||
|
|
||||||
|
# Process each rule found
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Extract service name from path_regex (e.g., "^/sonarr.*" -> "sonarr")
|
||||||
|
local path_regex=$(echo "$line" | grep -oP 'path: \K.*' | tr -d '"' | tr -d "'")
|
||||||
|
local policy=$(echo "$line" | grep -oP 'policy: \K.*' | tr -d '"' | tr -d "'")
|
||||||
|
|
||||||
|
if [[ "$path_regex" =~ ^\^/([a-zA-Z0-9_-]+)\.\* ]]; then
|
||||||
|
local service="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
# Skip duplicates if multiple rules somehow match the same pattern start
|
||||||
|
if [[ "$processed_services" == *"$service"* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
processed_services="$processed_services $service"
|
||||||
|
|
||||||
|
printf "${BOLD}%-20s${NC}" "$service"
|
||||||
|
case "$policy" in
|
||||||
|
"one_factor"|"two_factor")
|
||||||
|
echo -e "${GREEN}${policy}${NC}"
|
||||||
|
;;
|
||||||
|
"bypass")
|
||||||
|
echo -e "${YELLOW}${policy}${NC}"
|
||||||
|
;;
|
||||||
|
"deny")
|
||||||
|
echo -e "${RED}${policy}${NC}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${MAGENTA}Unknown ($policy)${NC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
service_count=$((service_count + 1))
|
||||||
|
fi
|
||||||
|
# Use yq -N to prevent splitting lines with spaces
|
||||||
|
done <<< "$(echo "$rules_data" | yq -N e '.' -)"
|
||||||
|
|
||||||
|
else
|
||||||
|
# Fallback to grep (less reliable)
|
||||||
|
echo -e "${YELLOW}Warning: yq not found, using grep (may miss services or show duplicates).${NC}"
|
||||||
|
# Find lines with path_regex, then try to get the policy line after
|
||||||
|
grep -n "path_regex: '^/.*" "$AUTHELIA_CONFIG" | while IFS=: read -r line_num line_content; do
|
||||||
|
if [[ "$line_content" =~ path_regex:\ \'^\/([a-zA-Z0-9_-]+)\.\*\' ]]; then
|
||||||
|
local service="${BASH_REMATCH[1]}"
|
||||||
|
# Skip duplicates
|
||||||
|
if [[ "$processed_services" == *"$service"* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
processed_services="$processed_services $service"
|
||||||
|
|
||||||
|
# Try to get policy from the next line (very fragile)
|
||||||
|
local policy_line_num=$((line_num + 1))
|
||||||
|
local policy=$(sed -n "${policy_line_num}p" "$AUTHELIA_CONFIG" | grep "policy:" | awk '{print $2}')
|
||||||
|
|
||||||
|
printf "${BOLD}%-20s${NC}" "$service"
|
||||||
|
if [ -n "$policy" ]; then
|
||||||
|
case "$policy" in
|
||||||
|
"one_factor"|"two_factor") echo -e "${GREEN}${policy}${NC}" ;;
|
||||||
|
"bypass") echo -e "${YELLOW}${policy}${NC}" ;;
|
||||||
|
"deny") echo -e "${RED}${policy}${NC}" ;;
|
||||||
|
*) echo -e "${MAGENTA}Unknown ($policy)${NC}" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo -e "${RED}Unknown (could not read policy)${NC}"
|
||||||
|
fi
|
||||||
|
service_count=$((service_count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $service_count -eq 0 ]; then
|
||||||
|
echo -e "${YELLOW}No services with path_regex rules found in $AUTHELIA_CONFIG.${NC}"
|
||||||
|
echo -e "${YELLOW}Only services explicitly defined with path_regex rules are listed.${NC}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cleanup_backups() {
|
||||||
|
print_header "Backup Files Cleanup"
|
||||||
|
|
||||||
|
echo -e "${BLUE}Searching for backup files...${NC}"
|
||||||
|
|
||||||
|
local env_backups=$(find . -maxdepth 1 -name ".env.*.bak" | sort)
|
||||||
|
local compose_backups=$(find . -maxdepth 1 -name "docker-compose.*.bak" | sort)
|
||||||
|
local authelia_backups=$(find ./authelia -maxdepth 1 -name "configuration.*.bak" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
local env_count=$(echo "$env_backups" | grep -c "^")
|
||||||
|
local compose_count=$(echo "$compose_backups" | grep -c "^")
|
||||||
|
local authelia_count=$(echo "$authelia_backups" | grep -c "^")
|
||||||
|
|
||||||
|
echo -e "${CYAN}Found:${NC}"
|
||||||
|
echo -e " - ${CYAN}$env_count .env backup files${NC}"
|
||||||
|
echo -e " - ${CYAN}$compose_count docker-compose.yml backup files${NC}"
|
||||||
|
echo -e " - ${CYAN}$authelia_count Authelia configuration backup files${NC}"
|
||||||
|
|
||||||
|
local total_count=$((env_count + compose_count + authelia_count))
|
||||||
|
|
||||||
|
if [ $total_count -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}No backup files found.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Do you want to delete all backup files? [y/N]:${NC}"
|
||||||
|
read -r answer
|
||||||
|
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${YELLOW}Would you like to keep the most recent backup of each type? [Y/n]:${NC}"
|
||||||
|
read -r keep_recent
|
||||||
|
|
||||||
|
if [[ "$keep_recent" =~ ^[Nn]$ ]]; then
|
||||||
|
echo -e "${RED}Backup cleanup cancelled.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $env_count -gt 1 ]; then
|
||||||
|
local keep_env=$(echo "$env_backups" | tail -1)
|
||||||
|
local del_env=$(echo "$env_backups" | grep -v "$keep_env")
|
||||||
|
echo -e "${BLUE}Keeping most recent .env backup: ${CYAN}$keep_env${NC}"
|
||||||
|
echo -e "${BLUE}Deleting ${CYAN}$((env_count - 1))${BLUE} older .env backups${NC}"
|
||||||
|
for file in $del_env; do
|
||||||
|
rm "$file"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $compose_count -gt 1 ]; then
|
||||||
|
local keep_compose=$(echo "$compose_backups" | tail -1)
|
||||||
|
local del_compose=$(echo "$compose_backups" | grep -v "$keep_compose")
|
||||||
|
echo -e "${BLUE}Keeping most recent docker-compose.yml backup: ${CYAN}$keep_compose${NC}"
|
||||||
|
echo -e "${BLUE}Deleting ${CYAN}$((compose_count - 1))${BLUE} older docker-compose.yml backups${NC}"
|
||||||
|
for file in $del_compose; do
|
||||||
|
rm "$file"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $authelia_count -gt 1 ]; then
|
||||||
|
local keep_authelia=$(echo "$authelia_backups" | tail -1)
|
||||||
|
local del_authelia=$(echo "$authelia_backups" | grep -v "$keep_authelia")
|
||||||
|
echo -e "${BLUE}Keeping most recent Authelia config backup: ${CYAN}$keep_authelia${NC}"
|
||||||
|
echo -e "${BLUE}Deleting ${CYAN}$((authelia_count - 1))${BLUE} older Authelia config backups${NC}"
|
||||||
|
for file in $del_authelia; do
|
||||||
|
rm "$file"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Cleanup completed with most recent backups retained.${NC}"
|
||||||
|
else
|
||||||
|
for file in $env_backups $compose_backups $authelia_backups; do
|
||||||
|
rm "$file"
|
||||||
|
done
|
||||||
|
echo -e "${GREEN}All $total_count backup files have been deleted.${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Interactive menu for managing Authelia policies
|
||||||
|
manage_authelia_policies() {
|
||||||
|
print_header "Authelia Policy Management"
|
||||||
|
|
||||||
|
if ! check_file "$AUTHELIA_CONFIG"; then
|
||||||
|
echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
echo -e "\n${BLUE}Choose an option:${NC}"
|
||||||
|
echo -e " ${CYAN}1. ${NC}List services and their current Authelia policy"
|
||||||
|
echo -e " ${CYAN}2. ${NC}Set Authelia policy for a service"
|
||||||
|
echo -e " ${CYAN}3. ${NC}Return to main menu"
|
||||||
|
echo
|
||||||
|
|
||||||
|
local choice
|
||||||
|
echo -e "${YELLOW}Enter your choice [1-3]: ${NC}"
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
list_authelia_services
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "${BLUE}Enter the service name to set the policy for (e.g., sonarr, radarr):${NC}"
|
||||||
|
read -r service
|
||||||
|
if [ -z "$service" ]; then
|
||||||
|
echo -e "${RED}No service name provided.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Select the desired policy for '$service':${NC}"
|
||||||
|
echo -e " ${CYAN}1. ${GREEN}one_factor${NC} (Requires login)"
|
||||||
|
echo -e " ${CYAN}2. ${GREEN}two_factor${NC} (Requires login + 2FA)"
|
||||||
|
echo -e " ${CYAN}3. ${YELLOW}bypass${NC} (No login required)"
|
||||||
|
echo -e " ${CYAN}4. ${RED}deny${NC} (Access denied)"
|
||||||
|
echo -e " ${CYAN}5. ${NC}Cancel"
|
||||||
|
|
||||||
|
local policy_choice
|
||||||
|
local policy=""
|
||||||
|
while true; do
|
||||||
|
echo -e "${YELLOW}Enter policy choice [1-5]: ${NC}"
|
||||||
|
read -r policy_choice
|
||||||
|
case "$policy_choice" in
|
||||||
|
1) policy="one_factor"; break ;;
|
||||||
|
2) policy="two_factor"; break ;;
|
||||||
|
3) policy="bypass"; break ;;
|
||||||
|
4) policy="deny"; break ;;
|
||||||
|
5) policy=""; break ;; # Cancel
|
||||||
|
*) echo -e "${RED}Invalid choice.${NC}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$policy" ]; then
|
||||||
|
echo -e "${YELLOW}Policy change cancelled.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call the function to set the policy
|
||||||
|
set_authelia_policy "$service" "$policy" "$AUTHELIA_CONFIG"
|
||||||
|
|
||||||
|
# Check the return status of set_authelia_policy
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}"
|
||||||
|
echo -e " ${CYAN}docker compose restart authelia${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed to set policy. Please check errors above.${NC}"
|
||||||
|
# Optionally offer to restore backup here
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Invalid choice. Please try again.${NC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo # Add a newline for better readability between menu iterations
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# PART 5: Authelia Account Management
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
manage_authelia_accounts() {
|
||||||
|
print_header "Authelia Account Management"
|
||||||
|
|
||||||
|
local users_file="${CONFIG_ROOT:-.}/authelia/users_database.yml"
|
||||||
|
|
||||||
|
if [ ! -f "$users_file" ]; then
|
||||||
|
echo -e "${RED}Error: users_database.yml not found at $users_file${NC}"
|
||||||
|
echo -e "${YELLOW}Would you like to create a new users database file? [y/N]:${NC}"
|
||||||
|
read -r answer
|
||||||
|
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
cat > "$users_file" <<EOL
|
||||||
|
# Authelia User Database
|
||||||
|
# Documentation: https://www.authelia.com/configuration/security/authentication/file/
|
||||||
|
|
||||||
|
users:
|
||||||
|
admin:
|
||||||
|
displayname: "Admin User"
|
||||||
|
password: "$argon2id$v=19$m=102400,t=1,p=8$PBf/L9l3s7LwN6jX/B3tVg$9+q3kL8VAbpWj9Gv9Z6uA5bA4zT1fB2fH3aD5c6b7e8"
|
||||||
|
email: admin@example.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- users
|
||||||
|
EOL
|
||||||
|
echo -e "${GREEN}Created new users_database.yml file${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Account management cancelled.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local backup_file="${users_file}.${TIMESTAMP}.bak"
|
||||||
|
cp "$users_file" "$backup_file"
|
||||||
|
echo -e "${BLUE}Backed up users database to ${backup_file}${NC}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
echo -e "\n${CYAN}${BOLD}Add Authelia User${NC}"
|
||||||
|
echo -e "${BLUE}Enter a username (or press Enter to finish):${NC}"
|
||||||
|
read -r username
|
||||||
|
|
||||||
|
if [ -z "$username" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "^[[:space:]]*${username}:" "$users_file"; then
|
||||||
|
echo -e "${YELLOW}Warning: User '${username}' already exists.${NC}"
|
||||||
|
echo -e "${YELLOW}Would you like to update this user? [y/N]:${NC}"
|
||||||
|
read -r answer
|
||||||
|
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}Skipping user '${username}'.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Enter display name for ${username}:${NC}"
|
||||||
|
read -r displayname
|
||||||
|
if [ -z "$displayname" ]; then
|
||||||
|
displayname="$username"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Enter email for ${username}:${NC}"
|
||||||
|
read -r email
|
||||||
|
if [ -z "$email" ]; then
|
||||||
|
email="${username}@example.com"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Select group(s) for ${username} (comma-separated, default: users):${NC}"
|
||||||
|
echo -e "${CYAN}Available groups: admins, users${NC}"
|
||||||
|
read -r groups
|
||||||
|
if [ -z "$groups" ]; then
|
||||||
|
groups="users"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -ra group_array <<< "$groups"
|
||||||
|
formatted_groups=""
|
||||||
|
for group in "${group_array[@]}"; do
|
||||||
|
formatted_groups+=" - $(echo $group | xargs)\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
generated_passphrase=$(generate_passphrase)
|
||||||
|
echo -e "${GREEN}Generated passphrase: ${BOLD}${generated_passphrase}${NC}"
|
||||||
|
echo -e "${YELLOW}Do you want to use this passphrase? [Y/n]:${NC}"
|
||||||
|
read -r use_generated
|
||||||
|
|
||||||
|
if [[ "$use_generated" =~ ^[Nn]$ ]]; then
|
||||||
|
echo -e "${BLUE}Enter a custom password for ${username}:${NC}"
|
||||||
|
read -rs password
|
||||||
|
if [ -z "$password" ]; then
|
||||||
|
echo -e "${RED}Error: Password cannot be empty.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
password="$generated_passphrase"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}Generating password hash...${NC}"
|
||||||
|
|
||||||
|
password_hash=$(docker compose run --rm authelia authelia crypto hash generate argon2 --password "$password" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$password_hash" ]; then
|
||||||
|
echo -e "${RED}Error: Failed to generate password hash. Is the authelia container available?${NC}"
|
||||||
|
echo -e "${YELLOW}Trying direct docker run method...${NC}"
|
||||||
|
password_hash=$(docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "$password" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$password_hash" ]; then
|
||||||
|
echo -e "${RED}Error: Both methods failed to generate a password hash. Skipping user.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
password_hash=$(echo "$password_hash" | sed 's/^Digest: //')
|
||||||
|
|
||||||
|
if [[ ! "$password_hash" =~ ^\$argon2id.*$ ]]; then
|
||||||
|
echo -e "${RED}Error: Generated hash does not have the expected format. Actual value:${NC}"
|
||||||
|
echo -e "${YELLOW}$password_hash${NC}"
|
||||||
|
echo -e "${RED}Skipping user creation.${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Password hash generated successfully.${NC}"
|
||||||
|
|
||||||
|
if grep -q "^[[:space:]]*${username}:" "$users_file"; then
|
||||||
|
sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file"
|
||||||
|
sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$users_file" <<EOL
|
||||||
|
${username}:
|
||||||
|
displayname: "${displayname}"
|
||||||
|
password: "${password_hash}"
|
||||||
|
email: ${email}
|
||||||
|
groups:
|
||||||
|
$(echo -e "$formatted_groups")
|
||||||
|
EOL
|
||||||
|
|
||||||
|
echo -e "${GREEN}User '${username}' added successfully!${NC}"
|
||||||
|
echo -e "${CYAN}Password: ${BOLD}${password}${NC}"
|
||||||
|
echo -e "${YELLOW}Please save this password securely.${NC}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}${BOLD}Account Management Complete!${NC}"
|
||||||
|
echo -e "${BLUE}Users file updated: ${users_file}${NC}"
|
||||||
|
echo -e "${BLUE}Backup saved as: ${backup_file}${NC}"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Would you like to restart Authelia to apply changes? [y/N]:${NC}"
|
||||||
|
read -r restart
|
||||||
|
if [[ "$restart" =~ ^[Yy]$ ]]; then
|
||||||
|
if docker compose restart authelia; then
|
||||||
|
echo -e "${GREEN}Authelia restarted successfully!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed to restart Authelia. Please restart manually.${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Remember to restart Authelia to apply these changes:${NC}"
|
||||||
|
echo -e "${CYAN} docker compose restart authelia${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# MAIN SCRIPT
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
# Function to display help message
|
||||||
|
show_help() {
|
||||||
|
print_header "Docker Compose NAS - Setup Tool"
|
||||||
|
echo -e "${BLUE}Usage: $0 [command] [arguments]${NC}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}Available Commands:${NC}"
|
||||||
|
echo -e " ${CYAN}update-env${NC} Update .env file from .env.example, preserving values."
|
||||||
|
echo -e " ${CYAN}update-authelia${NC} Update Authelia configuration (configuration.yml) from example,"
|
||||||
|
echo -e " applying domain settings from .env and preserving secrets."
|
||||||
|
echo -e " ${CYAN}update-services${NC} Update configurations for running *arr/qBittorrent/Bazarr containers"
|
||||||
|
echo -e " (sets URL base, extracts API keys to .env)."
|
||||||
|
echo -e " ${CYAN}manage-accounts${NC} Interactively add/update Authelia users in users_database.yml."
|
||||||
|
echo -e " ${CYAN}manage-policies${NC} Interactively manage Authelia access policies for services."
|
||||||
|
echo -e " ${CYAN}list-policies${NC} List services and their current Authelia policy."
|
||||||
|
echo -e " ${CYAN}set-policy <svc> <pol>${NC} Set Authelia policy for a service (e.g., 'one_factor', 'bypass')."
|
||||||
|
echo -e " ${CYAN}cleanup${NC} Interactively clean up old backup files (.bak)."
|
||||||
|
echo -e " ${CYAN}all${NC} Run 'update-env', 'update-authelia', and 'update-services'."
|
||||||
|
echo -e " ${CYAN}help${NC} Show this help message."
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}Examples:${NC}"
|
||||||
|
echo -e " $0 update-authelia"
|
||||||
|
echo -e " $0 set-policy sonarr one_factor"
|
||||||
|
echo -e " $0 set-policy radarr bypass"
|
||||||
|
echo -e " $0 manage-policies"
|
||||||
|
echo -e " $0 all"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${YELLOW}Note:${NC} Some commands require Docker to be running and may restart containers."
|
||||||
|
echo -e "${YELLOW}Policy changes require an Authelia restart ('docker compose restart authelia').${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if any arguments were provided
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Process command line arguments
|
||||||
|
case "$1" in
|
||||||
|
update-env)
|
||||||
|
update_env_file
|
||||||
|
;;
|
||||||
|
update-authelia)
|
||||||
|
update_authelia_config
|
||||||
|
;;
|
||||||
|
update-services)
|
||||||
|
update_service_configs
|
||||||
|
;;
|
||||||
|
manage-accounts)
|
||||||
|
manage_authelia_accounts # Interactive
|
||||||
|
;;
|
||||||
|
manage-policies)
|
||||||
|
manage_authelia_policies # Interactive
|
||||||
|
;;
|
||||||
|
list-policies)
|
||||||
|
list_authelia_services
|
||||||
|
;;
|
||||||
|
set-policy)
|
||||||
|
if [ -z "$2" ] || [ -z "$3" ]; then
|
||||||
|
echo -e "${RED}Error: Service name and policy are required.${NC}" >&2
|
||||||
|
echo -e "Usage: $0 set-policy <service_name> <policy>${NC}" >&2
|
||||||
|
echo -e "Valid policies: one_factor, two_factor, bypass, deny" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! check_file "$AUTHELIA_CONFIG"; then exit 1; fi
|
||||||
|
# Backup is handled within set_authelia_policy if needed
|
||||||
|
set_authelia_policy "$2" "$3" "$AUTHELIA_CONFIG"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}"
|
||||||
|
echo -e " ${CYAN}docker compose restart authelia${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
cleanup)
|
||||||
|
cleanup_backups
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
print_header "Running All Updates"
|
||||||
|
update_env_file
|
||||||
|
update_authelia_config
|
||||||
|
update_service_configs
|
||||||
|
echo -e "\n${GREEN}${BOLD}All core updates completed!${NC}"
|
||||||
|
echo -e "${BLUE}Review output for any required actions (e.g., setting new .env variables).${NC}"
|
||||||
|
echo -e "${BLUE}Consider running 'manage-accounts' if needed.${NC}"
|
||||||
|
echo -e "${YELLOW}Remember to restart relevant services or the full stack if necessary.${NC}"
|
||||||
|
;;
|
||||||
|
help|-h|--help)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Unknown command: $1${NC}" >&2
|
||||||
|
echo -e "${BLUE}Run '$0 help' for usage information.${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user