Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

965 changed files with 23589 additions and 47344 deletions

View File

@ -1 +0,0 @@
{"sessionId":"9cea077c-56da-4223-a765-be38c688106b","pid":1384,"procStart":"1332","acquiredAt":1780168110981}

10
.gitattributes vendored
View File

@ -1,10 +0,0 @@
# Paths excluded from release tarballs. `git archive` (used by
# scripts/release/make_release.sh) honours `export-ignore`, so these dev-only
# trees never ship in libreportal-<ver>.tar.gz.
scripts/unused export-ignore
scripts/release export-ignore
site export-ignore
docs export-ignore
.claude export-ignore
.gitignore export-ignore
.gitattributes export-ignore

11
.gitignore vendored
View File

@ -10,14 +10,3 @@
# Node dependencies — installed via `npm ci` at image build, never vendored. # Node dependencies — installed via `npm ci` at image build, never vendored.
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
# Release build output (scripts/release/make_release.sh).
/dist/
# Eleventy site build output + generator-produced data (scripts/gen-data.mjs).
# site/ is the legacy website location (active site now lives in
# containers/weblibreportal); these are build artifacts, not source.
site/dist/
site/src/_data/apps.json
site/src/_data/categories.json

View File

@ -1,19 +0,0 @@
# LibrePortal — agent notes
## Verify WebUI changes visually before marking them done
After changing anything user-visible in the WebUI (`containers/libreportal/frontend/`),
confirm it actually renders correctly — syntax checks and type-correctness don't
catch layout or visual regressions.
The maintainer's dev environment provides a headless screenshot helper, `lp-shot`,
that captures a WebUI route (or a single element, via a trailing CSS selector) to a
PNG for review:
```
lp-shot /admin/system # full route -> /tmp/webui-shot.png
lp-shot /admin/system /tmp/x.png 12 ".sys-strip" # just one element, crisp
```
Use it (and read the PNG) to self-check UI work instead of assuming it looks right or
asking the user to look. Skip it for purely backend/non-visual edits. If `lp-shot`
isn't present, fall back to asking the user for a screenshot.

View File

@ -7,7 +7,7 @@ to you — in plain language, so you can hold us to it.
You can **run, study, modify, share, and fully use 100% of LibrePortal — You can **run, study, modify, share, and fully use 100% of LibrePortal —
every feature — for free, forever.** The entire platform is licensed under every feature — for free, forever.** The entire platform is licensed under
the GNU AGPLv3 (see [LICENSE](../../LICENSE)). There are **no feature paywalls in the GNU AGPLv3 (see [LICENSE](LICENSE)). There are **no feature paywalls in
the software, no crippled "community edition," and no telemetry** phoning the software, no crippled "community edition," and no telemetry** phoning
home. home.
@ -17,16 +17,13 @@ If you self-host LibrePortal, you get everything. No asterisks.
Building and maintaining this takes real work, and we want to do it Building and maintaining this takes real work, and we want to do it
sustainably — without betraying a word of the above. So we charge only for sustainably — without betraying a word of the above. So we charge only for
things that **aren't the software** — and only for services we can run things that **aren't the software**:
*without ever needing to see your data*:
- **LibrePortal Connect** — optional services for the tricky parts, like reaching - **LibrePortal Cloud** — optional hosted services we run for you: remote
your server from your phone or keeping off-site backups. We work like a access (no port-forwarding), off-site encrypted backups, a free subdomain
courier carrying a sealed box: we move and store your data, but it stays with automatic HTTPS, phone notifications, and more.
locked and *you* hold the only key — we can't open it, and we never run your - **Managed hosting** — we run LibrePortal for you, if you'd rather not.
apps for you. - **Support** — priority help for those who want it.
- **Support** — free community help, plus optional paid priority support for
those who want guaranteed fast answers.
## The line we will not cross ## The line we will not cross

View File

@ -9,19 +9,11 @@ it all.
> ⚠️ **v0.1.0 — early days.** Expect rough edges while things settle. > ⚠️ **v0.1.0 — early days.** Expect rough edges while things settle.
## Why LibrePortal
Too many services today treat your data as theirs to take — quietly
overstepping boundaries that should never have been crossed. LibrePortal grew
out of frustration with that: it's a way to run the apps you depend on on
your own server, where your data stays yours. Privacy here isn't a feature to
toggle — it's the whole point.
## Free & open — forever ## Free & open — forever
The entire platform is **free software under the [GNU AGPLv3](LICENSE)**. The entire platform is **free software under the [GNU AGPLv3](LICENSE)**.
Self-host it and you get **everything** — every feature, no paywalls, no Self-host it and you get **everything** — every feature, no paywalls, no
telemetry. See [our Promise](docs/guide/promise.md) for exactly what that means. telemetry. See [our Promise](PROMISE.md) for exactly what that means.
## What you get ## What you get
@ -34,46 +26,24 @@ telemetry. See [our Promise](docs/guide/promise.md) for exactly what that means.
## Quick start ## Quick start
```bash ```bash
curl -fsSL https://get.libreportal.org/install.sh | sudo bash git clone https://gitea.scottwebstar.co.uk/Webstar/LibrePortal.git
cd LibrePortal
./init.sh
``` ```
This installs a versioned, checksum-verified release (Debian/Ubuntu, root). Put ## LibrePortal Cloud (optional)
data on separate disks with `--system-dir=` / `--containers-dir=` / `--backups-dir=`.
> The `get.libreportal.org` host is still being set up — until it's live, build a Self-hosting is free and complete. If you'd rather not run the fiddly parts
> release and install from it locally (see the docs below). yourself, **LibrePortal Cloud** offers them as paid, hosted services — remote
access, off-site backups, notifications, and more. **Every one has a free,
## Documentation self-hostable equivalent in this repo** — you pay for convenience, never to
unlock. [Our Promise](PROMISE.md) spells out exactly where that line sits.
- **[docs/guide/install-and-use.md](docs/guide/install-and-use.md)** — install, place data on separate disks/drives,
update, back up, uninstall.
- **[docs/contributing/development.md](docs/contributing/development.md)** — run a dev copy, cut stable/edge
releases, and test them before publishing.
## LibrePortal Connect (optional)
Self-hosting is free and complete. If you'd rather not fiddle with the tricky
parts — like reaching your server from your phone, or keeping off-site backups
— LibrePortal Connect will handle them for you. Here's the catch that makes us
different: we work like a **courier carrying a sealed box.** We move your data
between your devices and store backup copies, but it stays locked and *you*
hold the only key — we can't open it, and we never run your apps for you.
**Everything we offer, you can also set up yourself for free.**
[Our Promise](docs/guide/promise.md) spells out exactly where that line sits.
## Contributing ## Contributing
PRs welcome — see [CONTRIBUTING.md](docs/contributing/contributing.md). We use a lightweight PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). We use a lightweight
DCO sign-off (`git commit -s`), no CLA. DCO sign-off (`git commit -s`), no CLA.
## Acknowledgments
LibrePortal has been built from scratch since 2023. Its spark of inspiration
was a small installer script from Brian McGonagill (`OpenSourceIsAwesome`):
[gitlab.com/bmcgonag/docker_installs](https://gitlab.com/bmcgonag/docker_installs).
From that seed it grew start to finish — refined, extended, and refactored
into the platform it is today.
## License ## License
[GNU AGPLv3](LICENSE). What's open stays open. [GNU AGPLv3](LICENSE). What's open stays open.

View File

@ -1 +0,0 @@
0.2.0

View File

@ -2,4 +2,4 @@ TITLE=Backup
DESCRIPTION=Backup schedules, retention, and engine settings DESCRIPTION=Backup schedules, retention, and engine settings
ICON=backup ICON=backup
ORDER=3 ORDER=3
SUBCATEGORY_ORDER=backup_general,backup_retention,backup_engine SUBCATEGORY_ORDER=backup_general,backup_retention,backup_advanced

View File

@ -1,9 +1,8 @@
# ================================================================================ # ================================================================================
# Backup Engine - **ADVANCED** Engine-level knobs most users won't need to touch # Backup Advanced - **ADVANCED** Engine-level knobs most users won't need to touch
# ================================================================================ # ================================================================================
CFG_BACKUP_ENGINE=restic # Default Backup Engine - Fallback engine for new locations (each location can override) [restic:Restic|borg:BorgBackup|kopia:Kopia] CFG_BACKUP_ENGINE=restic # Default Backup Engine - Fallback engine for new locations (each location can override) [restic:restic|borg:BorgBackup|kopia:Kopia]
CFG_BACKUP_DEFAULT_PATH= # Default Backup Location - Base directory for locations set to Automatic path mode; each location lives in its own numbered subfolder (<path>/<id>). Empty = the LibrePortal backups root (own mount-able). CFG_BACKUP_STRATEGY=stop-snapshot-start # Backup Strategy - How containers are quiesced before snapshotting [stop-snapshot-start:Stop → snapshot → start (safe default)|pause-snapshot-unpause:Pause → snapshot → unpause (less downtime)|live:Live — snapshot while running (only with DB dump hooks)]
CFG_BACKUP_STRATEGY=auto # Backup Strategy - How containers are quiesced before snapshotting [auto:Automatic — live where safe, stop otherwise (recommended)|stop-snapshot-start:Stop → snapshot → start (always safe)|pause-snapshot-unpause:Pause → snapshot → unpause (less downtime)|live:Live — snapshot while running (force)]
CFG_BACKUP_VERIFY_AFTER=true # Verify After Backup - Run integrity check after each backup CFG_BACKUP_VERIFY_AFTER=true # Verify After Backup - Run integrity check after each backup
CFG_BACKUP_VERIFY_DATA_PERCENT=5 # Verify Data Sample % - Percentage of repo data to checksum-verify weekly CFG_BACKUP_VERIFY_DATA_PERCENT=5 # Verify Data Sample % - Percentage of repo data to checksum-verify weekly
CFG_BACKUP_PARALLEL_REPOS=true # Parallel Repos - Push to all enabled locations in parallel CFG_BACKUP_PARALLEL_REPOS=true # Parallel Repos - Push to all enabled locations in parallel

View File

@ -2,3 +2,4 @@
# Backup General - Scheduling # Backup General - Scheduling
# ================================================================================ # ================================================================================
CFG_BACKUP_CRONTAB_APP="0 5 * * *" # App Backup Schedule - Crontab schedule for application backups CFG_BACKUP_CRONTAB_APP="0 5 * * *" # App Backup Schedule - Crontab schedule for application backups
CFG_BACKUP_CRONTAB_APP_INTERVAL=3 # App Backup Interval - Minutes between app backup checks

View File

@ -2,23 +2,23 @@
# Edit via the Locations page on /backup, or directly here. # Edit via the Locations page on /backup, or directly here.
CFG_BACKUP_LOC_1_NAME="Local disk" # Location Name - Friendly label shown in the UI CFG_BACKUP_LOC_1_NAME="Local disk" # Location Name - Friendly label shown in the UI
CFG_BACKUP_LOC_1_ENABLED=true # Enabled - Snapshot to this location CFG_BACKUP_LOC_1_ENABLED=true # Enabled - Snapshot to this location
CFG_BACKUP_LOC_1_ENGINE=restic # Engine - Backup engine used at this location [restic:Restic|borg:BorgBackup|kopia:Kopia] **ADVANCED** CFG_BACKUP_LOC_1_ENGINE=restic # Engine - Backup engine used at this location [restic:restic|borg:BorgBackup|kopia:Kopia]
CFG_BACKUP_LOC_1_PASSWORD=RANDOMIZEDPASSWORD1 # Repository Password - Used to encrypt/decrypt snapshots — back up offline! CFG_BACKUP_LOC_1_PASSWORD=RANDOMIZEDPASSWORD1 # Repository Password - Used to encrypt/decrypt snapshots — back up offline!
CFG_BACKUP_LOC_1_TYPE=local # Type - Backend [local:Local / mounted path|sftp:SFTP|rest:REST|s3:S3|b2:Backblaze B2|gs:Google Cloud Storage|azure:Azure|rclone:rclone] CFG_BACKUP_LOC_1_TYPE=local # Type - Backend [local:Local / mounted path|sftp:SFTP|rest:REST|s3:S3|b2:Backblaze B2|gs:Google Cloud Storage|azure:Azure|rclone:rclone]
CFG_BACKUP_LOC_1_PATH_MODE=auto # Path Mode - Automatic uses the Default Backup Location from the Backup Engine config (one subfolder per location); Custom uses the path below [auto:Automatic|custom:Custom path] CFG_BACKUP_LOC_1_PATH_MODE=auto # Path Mode - Where this location stores its data [auto:Automatic (/docker/backups/<id>)|custom:Custom path]
CFG_BACKUP_LOC_1_PATH= # Custom Path - Filesystem path on this server (used when Path Mode = Custom) CFG_BACKUP_LOC_1_PATH= # Custom Path - Filesystem path on this server (used when Path Mode = Custom)
CFG_BACKUP_LOC_1_URI= # URI Override - Custom restic URI (leave blank to build from the fields below) **ADVANCED** CFG_BACKUP_LOC_1_URI= # URI Override - Custom restic URI (leave blank to build from the fields below)
CFG_BACKUP_LOC_1_SSH_USER= # SSH User - For sftp type CFG_BACKUP_LOC_1_SSH_USER= # SSH User - For sftp type
CFG_BACKUP_LOC_1_SSH_HOST= # SSH Host - For sftp type CFG_BACKUP_LOC_1_SSH_HOST= # SSH Host - For sftp type
CFG_BACKUP_LOC_1_SSH_PORT=22 # SSH Port - For sftp type **ADVANCED** CFG_BACKUP_LOC_1_SSH_PORT=22 # SSH Port - For sftp type
CFG_BACKUP_LOC_1_SSH_PATH= # SSH Remote Path - Path on the remote host where the repo lives CFG_BACKUP_LOC_1_SSH_PATH= # SSH Remote Path - Path on the remote host where the repo lives
CFG_BACKUP_LOC_1_SSH_AUTH=key # SSH Authentication - [key:SSH key (managed by LibrePortal)|password:Password (via sshpass)] CFG_BACKUP_LOC_1_SSH_AUTH=key # SSH Authentication - [key:SSH key (~/.ssh/id_rsa)|password:Password (via sshpass)]
CFG_BACKUP_LOC_1_SSH_PASS= # SSH Password - Used only when SSH Authentication is set to Password CFG_BACKUP_LOC_1_SSH_PASS= # SSH Password - Used only when SSH Authentication is set to Password
CFG_BACKUP_LOC_1_S3_ACCESS_KEY= # S3 Access Key - For s3 type CFG_BACKUP_LOC_1_S3_ACCESS_KEY= # S3 Access Key - For s3 type
CFG_BACKUP_LOC_1_S3_SECRET_KEY= # S3 Secret Key - For s3 type CFG_BACKUP_LOC_1_S3_SECRET_KEY= # S3 Secret Key - For s3 type
CFG_BACKUP_LOC_1_B2_ACCOUNT_ID= # B2 Account ID - For b2 type CFG_BACKUP_LOC_1_B2_ACCOUNT_ID= # B2 Account ID - For b2 type
CFG_BACKUP_LOC_1_B2_ACCOUNT_KEY= # B2 Account Key - For b2 type CFG_BACKUP_LOC_1_B2_ACCOUNT_KEY= # B2 Account Key - For b2 type
CFG_BACKUP_LOC_1_APPEND_ONLY=false # Append-only - Refuse forget/prune for this location (ransomware-safe) **ADVANCED** CFG_BACKUP_LOC_1_APPEND_ONLY=false # Append-only - Refuse forget/prune for this location (ransomware-safe)
CFG_BACKUP_LOC_1_CUSTOM_RETENTION=false # Custom Retention - Override the global retention for this location CFG_BACKUP_LOC_1_CUSTOM_RETENTION=false # Custom Retention - Override the global retention for this location
CFG_BACKUP_LOC_1_KEEP_LAST= # Keep Last - Snapshots to always retain (blank = global) CFG_BACKUP_LOC_1_KEEP_LAST= # Keep Last - Snapshots to always retain (blank = global)
CFG_BACKUP_LOC_1_KEEP_DAILY= # Keep Daily - Days (blank = global) CFG_BACKUP_LOC_1_KEEP_DAILY= # Keep Daily - Days (blank = global)

5
configs/features/.category Executable file
View File

@ -0,0 +1,5 @@
TITLE=Features
DESCRIPTION=Toggle system components and features
ICON=features
ORDER=5
SUBCATEGORY_ORDER=features_core,features_security,features_terminal

18
configs/features/features_core Executable file
View File

@ -0,0 +1,18 @@
# ================================================================================
# Core Features - Essential LibrePortal functionality and core services
# ================================================================================
CFG_REQUIREMENT_CONFIG=true # Configuration Management - Enable configuration management system for LibrePortal settings
CFG_REQUIREMENT_COMMAND=true # Command Line Tool - Install the libreportal command line tool for system management
CFG_REQUIREMENT_WEBUI=true # Web Interface - Install and manage the LibrePortal web based management interface
CFG_REQUIREMENT_WEBUI_SERVICE=true # Web Task Service - Install the task management systemd service for the web interface
CFG_REQUIREMENT_DATABASE=true # Database Support - Install and configure database support for application data storage
CFG_REQUIREMENT_PASSWORDS=true # Password Management - Enable password generation and management features
CFG_REQUIREMENT_DOCKER_CE=true # Docker CE - Install Docker Community Edition instead of the default Docker version
CFG_REQUIREMENT_DOCKER_COMPOSE=true # Docker Compose - Install Docker Compose for multi container application management
CFG_REQUIREMENT_DOCKER_NETWORK=true # Docker Network - Create and manage Docker network for container communication
CFG_REQUIREMENT_UFW=true # Firewall Protection - Install and configure the Uncomplicated Firewall for system security
CFG_REQUIREMENT_UFWD=true # Docker Firewall - Install UFW Docker for container aware firewall management which is rooted Docker specific
CFG_REQUIREMENT_SSLCERTS=true # SSL Certificates - Generate and manage SSL certificates for secure HTTPS connections
CFG_REQUIREMENT_CRONTAB=true # Scheduled Tasks - Setup scheduled tasks and automated maintenance jobs
CFG_REQUIREMENT_WHITELIST_PORT_UPDATER=true # Auto Port Management - Automatically update port whitelist when applications are installed or removed
CFG_REQUIREMENT_BCRYPT_SAVE=true # Password Encryption - Encrypt saved passwords using bcrypt for enhanced security

View File

@ -0,0 +1,7 @@
# ================================================================================
# Security and Authentication - SSH access and security configuration
# ================================================================================
CFG_REQUIREMENT_SSHKEY_DOWNLOADER=false # SSH Key Downloader - Enable SSH key download functionality for remote access
CFG_REQUIREMENT_SSH_DISABLE_PASSWORDS=false # SSH Password Disable - Disable password authentication for SSH requiring key based access only
CFG_REQUIREMENT_GLUETUN_FOR_ALL=false # Gluetun For All Apps - Allow routing through Gluetun VPN for every app (default: only curated categories)

View File

@ -0,0 +1,11 @@
# ================================================================================
# Terminal Only - Advanced terminal based features and utilities **ADVANCED**
# ================================================================================
CFG_REQUIREMENT_SUGGEST_INSTALLS=false # Install Suggestions - Enable application suggestions and recommendations during installation
CFG_REQUIREMENT_SUGGEST_METRICS=true # Metrics Suggestions - Offer Prometheus and Grafana during first install (requires Install Suggestions enabled)
CFG_REQUIREMENT_CONTINUE_PROMPT=false # Continue Prompts - Show continue prompts during installation for user confirmation
CFG_REQUIREMENT_CONFIGS_CHECK=true # Config Validation - Validate configuration files on startup for errors and consistency
CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE=true # Auto Config Updates - Automatically update configuration files when system changes are detected
CFG_REQUIREMENT_MISSING_IPS=false # IP Configuration Check - Check for and alert about missing IP configurations
CFG_REQUIREMENT_DOCKER_NETWORK_PRUNE=true # Docker Network Cleanup - Enable automatic cleanup of unused Docker networks
CFG_REQUIREMENT_DOCKER_SWITCHER=true # Docker Switcher - Install Docker version switching utility for managing multiple Docker versions

View File

@ -3,4 +3,3 @@
# ================================================================================ # ================================================================================
CFG_INSTALL_NAME=Change-Me # Installation Name - The name for your LibrePortal instance CFG_INSTALL_NAME=Change-Me # Installation Name - The name for your LibrePortal instance
CFG_TIMEZONE=Etc/UTC # System Timezone - Timezone for scheduled tasks and logging timestamps CFG_TIMEZONE=Etc/UTC # System Timezone - Timezone for scheduled tasks and logging timestamps
CFG_INSTALL_LEVEL=beginner # Experience Level - Beginner hides technical detail and skips advanced setup steps. Advanced reveals everything by default. Set during the first-run wizard; can be flipped any time via the Advanced toggle in the WebUI. [beginner:Beginner — simple|advanced:Advanced — show everything]

View File

@ -1,7 +1,7 @@
# ================================================================================ # ================================================================================
# Docker - Container runtime installation and configuration **ADVANCED** # Docker - Container runtime installation and configuration **ADVANCED**
# ================================================================================ # ================================================================================
CFG_DOCKER_INSTALL_TYPE=rootless # Docker Installation Type - rootless (default, recommended): containers run unprivileged so a breakout isn't host root; rooted: legacy, containers run as root [rootless|rooted] CFG_DOCKER_INSTALL_TYPE=rooted # Docker Installation Type - Security based setup rooted or rootless Docker installation [rooted|rootless]
CFG_DOCKER_INSTALL_USER=dockerinstall # Docker Install User - Username for Docker installation operations CFG_DOCKER_INSTALL_USER=dockerinstall # Docker Install User - Username for Docker installation operations
CFG_DOCKER_INSTALL_PASS=RANDOMIZEDPASSWORD2 # Docker Install Password - Password for Docker install user CFG_DOCKER_INSTALL_PASS=RANDOMIZEDPASSWORD2 # Docker Install Password - Password for Docker install user

View File

@ -1,20 +1,9 @@
# ================================================================================ # ================================================================================
# Installation Setup - Local or Git Repository configuration and version control # Installation Setup - Local or Git Repository configuration and version control
# ================================================================================ # ================================================================================
CFG_INSTALL_MODE=release # Installation Mode - How LibrePortal is fetched + updated. Hidden by default — only shown when Developer Mode is on (click the LibrePortal logo 10 times) or when the install is already on git/local (auto-enables Developer Mode). **DEV** [release:Release - Stable|git:Git clone|local:Local folder] CFG_INSTALL_MODE=local # Installation Mode - Method used for installation of LibrePortal
CFG_RELEASE_BASE_URL=https://get.libreportal.org # Release Host - Base URL serving the release channels (override for self-hosting) **ADVANCED**
CFG_RELEASE_CHANNEL=stable # Release Channel - Pick the release channel for the tarball installer. Stable is the recommended default; Edge ships from main and may contain in-flight changes. **DEV** [stable:Release - Stable|edge:Release - Bleeding Edge]
CFG_DEV_MODE=false # Developer Mode - Reveal advanced developer / dev-install options across the WebUI. Auto-enables when the install is already on git/local. Easter egg: click the LibrePortal logo 10 times to toggle. **ADVANCED** [true:On|false:Off]
CFG_GIT_URL=changeme # Git Repository URL - Git repository URL for LibrePortal configuration CFG_GIT_URL=changeme # Git Repository URL - Git repository URL for LibrePortal configuration
CFG_GIT_USER=changeme # Git Username - Git username for repository authentication CFG_GIT_USER=changeme # Git Username - Git username for repository authentication
CFG_GIT_KEY=changeme # Git Access Key - SSH key or API key for Git repository access CFG_GIT_KEY=changeme # Git Access Key - SSH key or API key for Git repository access
CFG_GIT_UPDATES=true # Auto Check Updates - Check for updates automatically CFG_GIT_UPDATES=true # Auto Check Updates - Check for Git repository updates automatically
CFG_GIT_AUTO_UPDATES=true # Auto Apply Updates - Automatically apply updates when available CFG_GIT_AUTO_UPDATES=true # Auto Apply Updates - Automatically apply Git updates when available
CFG_REQUIREMENT_CONFIG=true # Configuration Management - Install the configuration management system. Disabling this on an existing install will brick the system. **ADVANCED** **DEV**
CFG_REQUIREMENT_COMMAND=true # Command Line Tool - Install the libreportal command line tool. Disabling this on an existing install will brick the system. **ADVANCED** **DEV**
CFG_REQUIREMENT_WEBUI=true # Web Interface - Install the LibrePortal WebUI. Disabling this on an existing install will brick the system. **ADVANCED** **DEV**
CFG_REQUIREMENT_WEBUI_SERVICE=true # Web Task Service - Install the task-processor systemd service that backs the WebUI. Disabling this on an existing install will brick the system. **ADVANCED** **DEV**
CFG_REQUIREMENT_DATABASE=true # Database Support - Install and configure database support for application data storage. Install-time choice only — flipping post-install will not retrofit. **ADVANCED** **DEV**
CFG_REQUIREMENT_PASSWORDS=true # Password Management - Enable password generation and management features. Install-time choice only. **ADVANCED** **DEV**
CFG_REQUIREMENT_DOCKER_CE=true # Docker CE - Install Docker Community Edition instead of the distro default. Install-time choice only — flipping post-install does not swap Docker. **ADVANCED** **DEV**
CFG_REQUIREMENT_DOCKER_COMPOSE=true # Docker Compose - Install Docker Compose for multi-container application management. Install-time choice only. **ADVANCED** **DEV**

View File

@ -7,12 +7,3 @@ CFG_GENERATED_PASS_LENGTH=14 # Password Length - Len
CFG_GENERATED_USER_LENGTH=8 # Username Length - Length for auto generated usernames CFG_GENERATED_USER_LENGTH=8 # Username Length - Length for auto generated usernames
CFG_UFW_LOGGING=off # Firewall Logging - UFW firewall logging level [off|low|medium|high|full] CFG_UFW_LOGGING=off # Firewall Logging - UFW firewall logging level [off|low|medium|high|full]
CFG_TEXT_EDITOR=nano # Text Editor - Default text editor for system operations [nano|vim] CFG_TEXT_EDITOR=nano # Text Editor - Default text editor for system operations [nano|vim]
CFG_REQUIREMENT_CRONTAB=true # Scheduled Tasks - Install scheduled tasks and automated maintenance jobs
CFG_REQUIREMENT_CONFIGS_CHECK=true # Config Validation - Validate configuration files on startup for errors and consistency
CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE=true # Auto Config Updates - Add new config options from the template (non-interactive)
CFG_REQUIREMENT_CONFIGS_AUTO_DELETE=true # Auto Config Deletes - Remove config options no longer present in the template
CFG_REQUIREMENT_MISSING_IPS=false # IP Configuration Check - Check for and alert about missing IP configurations
CFG_REQUIREMENT_CONTINUE_PROMPT=false # Continue Prompts - Show continue prompts during installation for user confirmation
CFG_REQUIREMENT_CONTINUE_ON_ERROR=true # Continue On Error - Log failures to error_report.log and continue instead of aborting (on by default to surface issues; turn off for strict abort once clean)
CFG_REQUIREMENT_SUGGEST_INSTALLS=false # Install Suggestions - Enable application suggestions and recommendations during installation
CFG_REQUIREMENT_SUGGEST_METRICS=true # Metrics Suggestions - Offer Prometheus and Grafana during first install (requires Install Suggestions enabled)

View File

@ -2,4 +2,4 @@ TITLE=Network
DESCRIPTION=Network configuration and domain management DESCRIPTION=Network configuration and domain management
ICON=network ICON=network
ORDER=4 ORDER=4
SUBCATEGORY_ORDER=network_domains,network_whitelist,network_firewall,network_dns,network_docker,network_rootless,network_ports,network_headscale SUBCATEGORY_ORDER=network_domains,network_whitelist,network_dns,network_docker,network_ports,network_headscale

View File

@ -5,6 +5,3 @@
CFG_NETWORK_NAME=vpn # Network Name - Docker network name for container communication CFG_NETWORK_NAME=vpn # Network Name - Docker network name for container communication
CFG_NETWORK_SUBNET=10.100.0.0/16 # Network Subnet - Subnet range for Docker network CFG_NETWORK_SUBNET=10.100.0.0/16 # Network Subnet - Subnet range for Docker network
CFG_NETWORK_MTU=1500 # Network MTU - Maximum transmission unit for network packets CFG_NETWORK_MTU=1500 # Network MTU - Maximum transmission unit for network packets
CFG_REQUIREMENT_DOCKER_NETWORK=true # Docker Network - Create and manage the Docker network for container communication
CFG_REQUIREMENT_DOCKER_NETWORK_PRUNE=true # Network Cleanup - Automatically prune unused Docker networks
CFG_REQUIREMENT_DOCKER_SWITCHER=true # Docker Switcher - Install the Docker version switching utility

View File

@ -10,4 +10,3 @@ CFG_DOMAIN_6= # Domain 6 - Domain slo
CFG_DOMAIN_7= # Domain 7 - Domain slot 7 for a Traefik CFG_DOMAIN_7= # Domain 7 - Domain slot 7 for a Traefik
CFG_DOMAIN_8= # Domain 8 - Domain slot 8 for a Traefik CFG_DOMAIN_8= # Domain 8 - Domain slot 8 for a Traefik
CFG_DOMAIN_9= # Domain 9 - Domain slot 9 for a Traefik CFG_DOMAIN_9= # Domain 9 - Domain slot 9 for a Traefik
CFG_REQUIREMENT_SSLCERTS=true # SSL Certificates - Generate and manage SSL certificates for secure HTTPS connections **ADVANCED**

View File

@ -1,6 +0,0 @@
# ================================================================================
# Firewall - Host firewall and port-whitelist automation **ADVANCED**
# ================================================================================
CFG_REQUIREMENT_UFW=true # Firewall Protection - Install and configure the Uncomplicated Firewall (UFW) for system security
CFG_REQUIREMENT_UFWD=true # Docker Firewall - Install UFW-Docker for container-aware firewall management (rooted Docker only)
CFG_REQUIREMENT_WHITELIST_PORT_UPDATER=true # Auto Port Whitelisting - Update the port whitelist automatically when applications are installed or removed

View File

@ -1,5 +0,0 @@
# ================================================================================
# Rootless Networking - Network stack and behaviour for rootless Docker **ADVANCED**
# ================================================================================
CFG_ROOTLESS_NET=pasta # Rootless Network Driver - Network stack for rootless Docker. pasta (default): actively maintained, preserves the real client source IP on inbound connections, lower idle CPU; slirp4netns: legacy fallback, maintenance-only upstream. The matching rootlesskit port driver is selected automatically. On Debian, the installer also applies the local AppArmor override pasta needs (see scripts/docker/install/rootless/rootless_apparmor.sh) so this is a single-toggle switch. **ADVANCED** [pasta:Pasta (default, actively maintained)|slirp4netns:slirp4netns (legacy fallback)]

View File

@ -2,4 +2,4 @@ TITLE=Security
DESCRIPTION=Intrusion prevention, bouncers, and host firewall configuration DESCRIPTION=Intrusion prevention, bouncers, and host firewall configuration
ICON=security ICON=security
ORDER=5 ORDER=5
SUBCATEGORY_ORDER=security_logins,security_ssh SUBCATEGORY_ORDER=security_logins

View File

@ -1,7 +0,0 @@
# ================================================================================
# SSH & Access Hardening - Secure remote access and auth-storage toggles **ADVANCED**
# ================================================================================
CFG_REQUIREMENT_SSHKEY_DOWNLOADER=false # SSH Key Downloader - Enable SSH key download functionality for remote access
CFG_REQUIREMENT_SSH_DISABLE_PASSWORDS=false # Disable SSH Passwords - Disable password authentication for SSH (requires key-based access only)
CFG_REQUIREMENT_BCRYPT_SAVE=true # Password Encryption - Encrypt saved passwords using bcrypt for enhanced security
CFG_REQUIREMENT_GLUETUN_FOR_ALL=false # Gluetun For All Apps - Allow routing through Gluetun VPN for every app (default: only curated categories)

View File

@ -2,4 +2,4 @@ TITLE=WebUI
DESCRIPTION=Web interface settings and preferences DESCRIPTION=Web interface settings and preferences
ICON=webui ICON=webui
ORDER=2 ORDER=2
SUBCATEGORY_ORDER=webui_logins,webui_logs,webui_updater SUBCATEGORY_ORDER=webui_logins,webui_logs

View File

@ -1,5 +0,0 @@
# ================================================================================
# WebUI Updater - Automatic app update, CVE & improvement scanning **ADVANCED**
# ================================================================================
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans. 0 disables.
CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off]

View File

@ -14,7 +14,6 @@
# #
CFG_ADGUARD_APP_NAME=adguard CFG_ADGUARD_APP_NAME=adguard
CFG_ADGUARD_BACKUP=true CFG_ADGUARD_BACKUP=true
CFG_ADGUARD_BACKUP_STRATEGY=auto
CFG_ADGUARD_COMPOSE_FILE=default CFG_ADGUARD_COMPOSE_FILE=default
CFG_ADGUARD_HEALTHCHECK=true CFG_ADGUARD_HEALTHCHECK=true
CFG_ADGUARD_AUTHELIA=false CFG_ADGUARD_AUTHELIA=false
@ -44,10 +43,12 @@ CFG_ADGUARD_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips on traefik, if false allow all # WHITELIST = if true only allow whitelisted ips on traefik, if false allow all
# #
CFG_ADGUARD_DOMAIN=1 CFG_ADGUARD_DOMAIN=1
CFG_ADGUARD_WHITELIST=false CFG_ADGUARD_WHITELIST=false
CFG_ADGUARD_HOST_NAME=adguard
CFG_ADGUARD_NETWORK=default CFG_ADGUARD_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -64,7 +65,7 @@ CFG_ADGUARD_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_ADGUARD_PORT_1="adguard-service|webui|random:3000|public|tcp|true|true|true|Admin Interface||adguard" CFG_ADGUARD_PORT_1="adguard-service|webui|random:3000|public|tcp|true|true|true|Admin Interface|"
CFG_ADGUARD_PORT_2="adguard-service|dns-tcp|random:53|public|tcp|false|false|false|DNS Server (TCP)|" CFG_ADGUARD_PORT_2="adguard-service|dns-tcp|random:53|public|tcp|false|false|false|DNS Server (TCP)|"
CFG_ADGUARD_PORT_3="adguard-service|dns-udp|random:53|public|udp|false|false|false|DNS Server (UDP)|" CFG_ADGUARD_PORT_3="adguard-service|dns-udp|random:53|public|udp|false|false|false|DNS Server (UDP)|"
CFG_ADGUARD_PORT_4="adguard-service|dns-alt|random:8053|disabled|tcp|false|false|false|Alternative DNS|" CFG_ADGUARD_PORT_4="adguard-service|dns-alt|random:8053|disabled|tcp|false|false|false|Alternative DNS|"

View File

@ -0,0 +1,276 @@
#!/bin/bash
# Category : Networking
# Description : AdGuard - DNS based Ad Blocking (c/u/s/r/i):
installAdguard()
{
local config_variables="$1"
if [[ "$adguard" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent adguard;
local app_name=$CFG_ADGUARD_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$adguard" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$adguard" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$adguard" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$adguard" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$adguard" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
monitoringToggleAppConfig "$app_name" "docker-compose.yml";
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Completing AdGuardHome initial setup automatically"
echo ""
# The legacy `$usedport1` variable is no longer populated by the
# current install pipeline; the resolved host port is stored in the
# PORTS_TAG_1 docker-compose tag (format `external:internal`). Pull
# it from there so the curl + URL printout actually point somewhere.
local adguard_compose_file="$containers_dir$app_name/docker-compose.yml"
local adguard_port_pair
adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1")
local adguard_admin_port="${adguard_port_pair%%:*}"
if [[ -n "$public_ip_v4" && -n "$adguard_admin_port" ]]; then
echo " External : http://$public_ip_v4:$adguard_admin_port/"
fi
if [[ -n "$host_setup" ]]; then
echo " Hostname : http://$host_setup/"
fi
echo ""
# AdGuardHome ships a setup wizard that normally needs five clicks in a
# browser before the daemon writes its config file. Same wizard is
# exposed as an HTTP API (POST /control/install/configure), so we
# drive it from here and skip the manual interaction. We pre-poll the
# admin endpoint until the container is up, then send the form, then
# let the existing post-install sed edits run against the freshly
# written AdGuardHome.yaml.
local adguard_setup_url="http://127.0.0.1:${adguard_admin_port}"
local adguard_attempts=0
local adguard_max_attempts=60
while ((adguard_attempts < adguard_max_attempts)); do
if curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/status" 2>/dev/null \
|| curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/install/get_addresses" 2>/dev/null; then
break
fi
sleep 2
((adguard_attempts++))
done
if ((adguard_attempts >= adguard_max_attempts)); then
isError "AdGuardHome admin endpoint did not respond on $adguard_setup_url within $((adguard_max_attempts * 2))s — open the URL and complete setup manually, then re-run the installer to apply the post-setup tweaks."
else
local adguard_user="${CFG_ADGUARD_USER:-admin}"
local adguard_pass="${CFG_ADGUARD_PASSWORD:-}"
if [[ -z "$adguard_pass" ]]; then
adguard_pass=$(generateRandomPassword)
updateConfigOption "CFG_ADGUARD_PASSWORD" "$adguard_pass" >/dev/null 2>&1 || true
isNotice "Generated a random AdGuardHome admin password and saved it to CFG_ADGUARD_PASSWORD."
fi
# Internal container ports are fixed (3000 admin, 53 DNS); host
# mapping is what `usedport1` etc. handle.
local adguard_payload
adguard_payload=$(cat <<JSON
{
"web": { "ip": "0.0.0.0", "port": 3000, "autofix": false },
"dns": { "ip": "0.0.0.0", "port": 53, "autofix": false },
"username": "${adguard_user}",
"password": "${adguard_pass}"
}
JSON
)
if curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data "$adguard_payload" \
--max-time 15 \
"${adguard_setup_url}/control/install/configure" >/dev/null 2>&1; then
isSuccessful "AdGuardHome admin setup completed automatically (user: $adguard_user)."
else
# 422/403 here typically means setup was already done on a
# previous install; the post-setup tweaks below are still
# safe to run against the existing yaml.
isNotice "AdGuardHome /control/install/configure rejected the request — assuming it's already configured. If this is a fresh install, complete setup manually at $adguard_setup_url."
fi
# Small breather so AdGuardHome finishes flushing AdGuardHome.yaml
# to disk before the sed edits below touch it.
#sleep 3
fi
#result=$(sudo sed -i "s/address: 0.0.0.0:80/address: 0.0.0.0:${usedport2}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
#checkSuccess "Changing port 80 to $usedport2 for Admin Panel"
#result=$(sudo sed -i "s/port: 53/port: ${usedport3}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
#checkSuccess "Changing port 53 to $usedport3 for DNS Port"
#result=$(sudo sed -i "s/port_https: 443/port_https: ${usedport4}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
#checkSuccess "Changing port 443 to $usedport4 for DNS Port"
#result=$(sudo sed -i "s/port_dns_over_tls: 853/port_dns_over_tls: ${usedport5}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
#checkSuccess "Changing port 853 to $usedport5 for port_dns_over_tls"
#result=$(sudo sed -i "s/port_dns_over_quic: 853/port_dns_over_quic: ${usedport5}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
#checkSuccess "Changing port 853 to $usedport5 for port_dns_over_quic"
# NOTE: We deliberately do *not* force `tls.enabled: true` here.
# That section configures encrypted DNS (DoT/DoH/DoQ) and AdGuardHome
# crash-loops on startup with `[fatal] creating dns server: parsing
# tls key pair: tls: failed to find any PEM data in certificate input`
# if `enabled: true` is set without a real certificate pair pointed
# at by `certificate_path` / `private_key_path`. The admin user can
# opt into encrypted DNS from Settings → Encryption once they've
# provided a cert.
if [[ $public == "true" ]]; then
result=$(sudo sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting allow_unencrypted_doh to false for Traefik"
fi
result=$(sudo sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting anonymize_client_ip to true for privacy reasons"
# Force the admin web bind back to 0.0.0.0:3000 inside the container.
# The docker-compose mapping is `<host_port>:3000`, so the container
# MUST listen on 3000 internally for the host port to reach it. After
# the install API call AdGuardHome sometimes ends up bound to 0.0.0.0:80
# (its build-time default) instead of the port we sent — which is
# exactly what causes "unable to connect" on the host port.
local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml"
if [[ -f "$adguard_yaml" ]]; then
# New schema (v0.107+): single `address: 0.0.0.0:NN` line under `http:`.
sudo sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml"
# Old schema fallback: separate `bind_host:` / `bind_port:` keys.
sudo sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml"
sudo sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml"
checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)."
fi
dockerComposeRestart "$app_name";
# Health-check after the restart so the user finds out *here* if
# AdGuardHome didn't come back up cleanly, rather than later when
# they try to open the URL and just see "unable to connect".
#
# Drop `-f` and accept any HTTP status code: now that the admin
# account is configured, `/control/status` returns 401 to an
# unauthenticated request — which is fine, it means the server is
# up and answering. We only care whether the connection succeeded
# at all, not what the response body says. `-w '%{http_code}'`
# gives us a 3-digit code on success and an empty string on a
# connection failure / timeout.
local adguard_health_attempts=0
while ((adguard_health_attempts < 20)); do
local adguard_health_code
adguard_health_code=$(curl -sS -o /dev/null --max-time 2 \
-w '%{http_code}' "${adguard_setup_url}/control/status" 2>/dev/null)
if [[ "$adguard_health_code" =~ ^[1-5][0-9][0-9]$ ]]; then
isSuccessful "AdGuardHome admin UI is reachable on $adguard_setup_url (HTTP $adguard_health_code)"
break
fi
sleep 1
((adguard_health_attempts++))
done
if ((adguard_health_attempts >= 20)); then
isError "AdGuardHome admin UI did not respond after restart on $adguard_setup_url. Check the container logs (\`docker logs adguard-service\`) and the conf/AdGuardHome.yaml bind address."
fi
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating the WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing monitoring integration."
echo ""
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
# Same final-summary call shape as wireguard / vaultwarden. Pass the
# admin user/password we just configured so the user sees the
# credentials exactly once, at the end of the install.
menuShowFinalMessages "$app_name" "${CFG_ADGUARD_USER:-admin}" "$CFG_ADGUARD_PASSWORD";
menu_number=0
#sleep 3s
cd
fi
adguard=n
}

View File

@ -20,15 +20,13 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.adguard-service-webui.entrypoints: web,websecure traefik.http.routers.adguard-service-webui.entrypoints: web,websecure
traefik.http.routers.adguard-service-webui.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.adguard-service-webui.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.adguard-service-webui.service: adguard-service-webui traefik.http.routers.adguard-service-webui.service: adguard-service-webui
traefik.http.routers.adguard-service-webui.tls: true traefik.http.routers.adguard-service-webui.tls: true
traefik.http.routers.adguard-service-webui.tls.certresolver: production traefik.http.routers.adguard-service-webui.tls.certresolver: production
traefik.http.services.adguard-service-webui.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.adguard-service-webui.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.adguard-service-webui.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.adguard-service-webui.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
# TRAEFIK_PORT_1_END
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
volumes: volumes:

View File

@ -1,30 +0,0 @@
#!/bin/bash
authAdapter_adguard_setPassword() {
local user="$1" password="$2"
user="${user:-${CFG_ADGUARD_ADMIN_USER:-admin}}"
[[ -z "$password" ]] && password=$(generateRandomPassword)
local yaml="${containers_dir}adguard/conf/AdGuardHome.yaml"
[[ ! -f "$yaml" ]] && { isError "AdGuardHome.yaml not found at $yaml."; return 1; }
if ! command -v htpasswd >/dev/null 2>&1; then
isError "htpasswd is required to bcrypt the new password."
return 1
fi
local bcrypt
bcrypt=$(htpasswd -bnBC 10 "" "$password" | tr -d ':\n')
[[ -z "$bcrypt" ]] && { isError "bcrypt failed."; return 1; }
# The yaml is owned by the in-container uid, so the rewrite runs in the
# root-owned appcfg helper (fixed path, validated user + bcrypt).
if ! runAppCfg adguard-auth "$user" "$bcrypt"; then
isError "Could not update AdGuardHome.yaml (no users password line, or invalid input)."
return 1
fi
authPersistCfg adguard ADMIN_USER "$user"
authPersistCfg adguard ADMIN_PASSWORD "$password"
dockerComposeRestart adguard
isSuccessful "AdGuard admin set. User: $user — Password: $password"
}

View File

@ -1,142 +0,0 @@
#!/bin/bash
# AdGuard Home install hooks — drive the first-boot setup wizard via its
# HTTP API so the admin doesn't have to click through five pages, then
# pin the admin bind back to 0.0.0.0:3000 (matches the compose mapping)
# and health-check the result.
adguard_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Completing AdGuardHome initial setup automatically"
echo ""
# The legacy `$usedport1` variable isn't populated by the current
# install pipeline; the resolved host port is stored in the PORTS_TAG_1
# docker-compose tag (format `external:internal`). Pull it from there
# so the curl + URL printout actually point somewhere real.
local adguard_compose_file="$containers_dir$app_name/docker-compose.yml"
local adguard_port_pair
adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1")
local adguard_admin_port="${adguard_port_pair%%:*}"
if [[ -n "$public_ip_v4" && -n "$adguard_admin_port" ]]; then
echo " External : http://$public_ip_v4:$adguard_admin_port/"
fi
if [[ -n "$host_setup" ]]; then
echo " Hostname : http://$host_setup/"
fi
echo ""
# AdGuardHome ships a setup wizard that normally needs five clicks in
# a browser before the daemon writes its config file. Same wizard is
# exposed as an HTTP API (POST /control/install/configure), so drive
# it from here and skip the manual interaction. Pre-poll the admin
# endpoint until the container is up, then send the form, then let
# the post-install sed edits run against the freshly written
# AdGuardHome.yaml.
local adguard_setup_url="http://127.0.0.1:${adguard_admin_port}"
local adguard_attempts=0
local adguard_max_attempts=60
while ((adguard_attempts < adguard_max_attempts)); do
if curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/status" 2>/dev/null \
|| curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/install/get_addresses" 2>/dev/null; then
break
fi
sleep 2
((adguard_attempts++))
done
if ((adguard_attempts >= adguard_max_attempts)); then
isError "AdGuardHome admin endpoint did not respond on $adguard_setup_url within $((adguard_max_attempts * 2))s — open the URL and complete setup manually, then re-run the installer to apply the post-setup tweaks."
else
local adguard_user="${CFG_ADGUARD_USER:-admin}"
local adguard_pass="${CFG_ADGUARD_PASSWORD:-}"
if [[ -z "$adguard_pass" ]]; then
adguard_pass=$(generateRandomPassword)
updateConfigOption "CFG_ADGUARD_PASSWORD" "$adguard_pass" >/dev/null 2>&1 || true
isNotice "Generated a random AdGuardHome admin password and saved it to CFG_ADGUARD_PASSWORD."
fi
# Internal container ports are fixed (3000 admin, 53 DNS); host
# mapping is what `usedport1` etc. handle.
local adguard_payload
adguard_payload=$(cat <<JSON
{
"web": { "ip": "0.0.0.0", "port": 3000, "autofix": false },
"dns": { "ip": "0.0.0.0", "port": 53, "autofix": false },
"username": "${adguard_user}",
"password": "${adguard_pass}"
}
JSON
)
if curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data "$adguard_payload" \
--max-time 15 \
"${adguard_setup_url}/control/install/configure" >/dev/null 2>&1; then
isSuccessful "AdGuardHome admin setup completed automatically (user: $adguard_user)."
else
# 422/403 here typically means setup was already done on a
# previous install; the post-setup tweaks below are still
# safe to run against the existing yaml.
isNotice "AdGuardHome /control/install/configure rejected the request — assuming it's already configured. If this is a fresh install, complete setup manually at $adguard_setup_url."
fi
fi
local result
if [[ "$public" == "true" ]]; then
result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting allow_unencrypted_doh to false for Traefik"
fi
result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting anonymize_client_ip to true for privacy reasons"
# Force the admin web bind back to 0.0.0.0:3000 inside the container.
# The docker-compose mapping is `<host_port>:3000`, so the container
# MUST listen on 3000 internally for the host port to reach it. After
# the install API call AdGuardHome sometimes ends up bound to
# 0.0.0.0:80 (its build-time default) — exactly what causes "unable
# to connect" on the host port.
local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml"
if [[ -f "$adguard_yaml" ]]; then
runFileOp sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml"
runFileOp sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml"
runFileOp sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml"
checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)."
fi
dockerComposeRestart "$app_name"
# Drop `-f` and accept any HTTP status code: now that the admin
# account is configured, /control/status returns 401 to an
# unauthenticated request — which is fine, it means the server is up
# and answering. We only care whether the connection succeeded at
# all, not what the response body says.
local adguard_health_attempts=0
local adguard_health_code
while ((adguard_health_attempts < 20)); do
adguard_health_code=$(curl -sS -o /dev/null --max-time 2 \
-w '%{http_code}' "${adguard_setup_url}/control/status" 2>/dev/null)
if [[ "$adguard_health_code" =~ ^[1-5][0-9][0-9]$ ]]; then
isSuccessful "AdGuardHome admin UI is reachable on $adguard_setup_url (HTTP $adguard_health_code)"
break
fi
sleep 1
((adguard_health_attempts++))
done
if ((adguard_health_attempts >= 20)); then
isError "AdGuardHome admin UI did not respond after restart on $adguard_setup_url. Check the container logs (\`docker logs adguard-service\`) and the conf/AdGuardHome.yaml bind address."
fi
}
adguard_install_message_data()
{
# Echo the admin user + password as space-separated tokens so they
# become $username $password positional args to menuShowFinalMessages.
echo "${CFG_ADGUARD_USER:-admin} $CFG_ADGUARD_PASSWORD"
}

View File

@ -1,12 +0,0 @@
#!/bin/bash
# Post-install/update specifics for AdGuard Home — dispatched by appUpdateSpecifics
# (containers/<app>/scripts/<app>_update_specifics.sh defining appUpdateSpecifics_<app>).
appUpdateSpecifics_adguard() {
local app_name="$1"
if [[ $CFG_REQUIREMENT_DNS_UPDATER == "true" ]]; then
updateDNS "$app_name" install
fi
# Split-horizon local DNS: app subdomains resolve to the box on the LAN.
declare -F setupLocalDnsRewrites >/dev/null 2>&1 && setupLocalDnsRewrites
}

View File

@ -1,20 +0,0 @@
{
"tools": [
{
"id": "reset_password",
"label": "Reset Admin Password",
"description": "Set a new admin password. Leave blank to generate one.",
"icon": "🔑",
"fields": [
{ "name": "password", "label": "New password", "type": "password", "placeholder": "Leave blank to generate" }
]
},
{
"id": "apply_dns_updater",
"label": "Apply DNS Updater",
"description": "Point this server's DNS at AdGuard now.",
"icon": "🌐",
"fields": []
}
]
}

View File

@ -17,7 +17,6 @@
CFG_AUTHELIA_APP_NAME=authelia CFG_AUTHELIA_APP_NAME=authelia
CFG_AUTHELIA_REQUIRES="domain,traefik" CFG_AUTHELIA_REQUIRES="domain,traefik"
CFG_AUTHELIA_BACKUP=true CFG_AUTHELIA_BACKUP=true
CFG_AUTHELIA_BACKUP_STRATEGY=auto
CFG_AUTHELIA_COMPOSE_FILE=default CFG_AUTHELIA_COMPOSE_FILE=default
CFG_AUTHELIA_HEALTHCHECK=true CFG_AUTHELIA_HEALTHCHECK=true
CFG_AUTHELIA_AUTHELIA=false CFG_AUTHELIA_AUTHELIA=false
@ -50,10 +49,12 @@ CFG_AUTHELIA_REQUIRES_SERVICE=traefik
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_AUTHELIA_DOMAIN=1 CFG_AUTHELIA_DOMAIN=1
CFG_AUTHELIA_WHITELIST=false CFG_AUTHELIA_WHITELIST=false
CFG_AUTHELIA_HOST_NAME=authelia
CFG_AUTHELIA_NETWORK=default CFG_AUTHELIA_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -70,4 +71,4 @@ CFG_AUTHELIA_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_AUTHELIA_PORT_1="authelia-service|webui|random:9091|public|tcp|false|true|true|Web Interface||authelia" CFG_AUTHELIA_PORT_1="authelia-service|webui|random:9091|public|tcp|false|true|true|Web Interface|"

208
containers/authelia/authelia.sh Executable file
View File

@ -0,0 +1,208 @@
#!/bin/bash
# Category : Security
# Description : Authelia - Authentication & SSO (c/u/s/r/i):
installAuthelia()
{
local config_variables="$1"
if [[ "$authelia" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent authelia;
local app_name=$CFG_AUTHELIA_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$authelia" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$authelia" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$authelia" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$authelia" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$authelia" == *[iI]* ]]; then
isHeader "Install $app_name"
# Pre-flight: bail out before touching any compose/config if the
# global prerequisites aren't met. CFG_AUTHELIA_REQUIRES lists
# what's needed (currently "domain,traefik"); the helper prints a
# clear list of what's missing so the user knows what to fix.
if ! appInstallCheckRequirements "$app_name" "$CFG_AUTHELIA_REQUIRES"; then
authelia=n
return 1
fi
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
local result=$(copyResource "$app_name" "configuration.yml" "config" | sudo tee -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying configuration.yml to $containers_dir$app_name/config"
local result=$(copyResource "$app_name" "users_database.yml" "config" | sudo tee -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying users_database.yml to $containers_dir$app_name/config"
local authelia_config_file="$containers_dir$app_name/config/configuration.yml"
sudo sed -i "s|AUTHELIA_THEME_PLACEHOLDER|$CFG_AUTHELIA_THEME|g" "$authelia_config_file"
sudo sed -i "s|AUTHELIA_DOMAIN_PLACEHOLDER|$domain_full|g" "$authelia_config_file"
sudo sed -i "s|AUTHELIA_HOST_PLACEHOLDER|$host_setup|g" "$authelia_config_file"
checkSuccess "Substituting Authelia configuration values (theme=$CFG_AUTHELIA_THEME domain=$domain_full host=$host_setup)"
local authelia_secrets_dir="$containers_dir$app_name/secrets"
sudo mkdir -p "$authelia_secrets_dir"
for secret_name in JWT_SECRET SESSION_SECRET STORAGE_ENCRYPTION_KEY; do
local secret_file="$authelia_secrets_dir/$secret_name"
if [[ ! -s "$secret_file" ]]; then
openssl rand -hex 64 | sudo tee "$secret_file" >/dev/null
sudo chmod 600 "$secret_file"
fi
done
sudo chown -R "$docker_install_user":"$docker_install_user" "$authelia_secrets_dir"
checkSuccess "Generated Authelia secrets at $authelia_secrets_dir"
# Enable Authelia's telemetry/metrics endpoint only when
# CFG_AUTHELIA_MONITORING=true (toggles the libreportal-monitoring
# marker block in configuration.yml).
monitoringToggleAppConfig "$app_name" "config/configuration.yml";
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Configuring Authelia admin account"
echo ""
local authelia_admin_user="${CFG_AUTHELIA_ADMIN_USERNAME:-admin}"
local authelia_admin_pass="${CFG_AUTHELIA_ADMIN_PASSWORD:-authelia}"
local authelia_users_file="$containers_dir$app_name/config/users_database.yml"
local authelia_attempts=0
while ((authelia_attempts < 30)); do
if sudo docker exec authelia-service authelia --version >/dev/null 2>&1; then
break
fi
sleep 2
((authelia_attempts++))
done
if ((authelia_attempts >= 30)); then
isNotice "Authelia container did not become responsive in time — admin left at default (admin / authelia)."
else
local authelia_hash
authelia_hash=$(sudo docker exec authelia-service authelia crypto hash generate argon2 --password "$authelia_admin_pass" 2>/dev/null \
| grep -oE '\$argon2[^[:space:]]+')
if [[ -z "$authelia_hash" ]]; then
isNotice "Could not generate Authelia password hash — admin left at default (admin / authelia)."
else
sudo tee "$authelia_users_file" >/dev/null <<EOF
---
users:
${authelia_admin_user}:
disabled: false
displayname: "Admin"
password: "${authelia_hash}"
email: ${authelia_admin_user}@${domain_full:-example.com}
groups:
- admins
EOF
sudo chown "$docker_install_user":"$docker_install_user" "$authelia_users_file"
isSuccessful "Configured Authelia admin (user: $authelia_admin_user)."
dockerComposeRestart "$app_name";
fi
fi
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing monitoring integration."
echo ""
# Self-correcting: adds Authelia's scrape target + dashboard to
# Prometheus/Grafana when CFG_AUTHELIA_MONITORING=true, removes them
# when it's off. No-ops with a notice if either app isn't installed.
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your new service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
echo ""
isNotice "Authelia admin login:"
echo ""
echo " Username : ${authelia_admin_user}"
echo " Password : ${authelia_admin_pass}"
echo ""
menu_number=0
#sleep 3s
cd
fi
authelia=n
}

View File

@ -23,16 +23,13 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "sqlite:::config/db.sqlite3"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.authelia-service.entrypoints: web,websecure traefik.http.routers.authelia-service.entrypoints: web,websecure
traefik.http.routers.authelia-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.authelia-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.authelia-service.tls: true traefik.http.routers.authelia-service.tls: true
traefik.http.routers.authelia-service.tls.certresolver: production traefik.http.routers.authelia-service.tls.certresolver: production
traefik.http.services.authelia-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.authelia-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.authelia-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.authelia-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA

View File

@ -1,111 +0,0 @@
#!/bin/bash
# Authelia install hooks — requirements check, config + secrets bootstrap,
# admin account provisioning, and an end-of-install credentials notice.
authelia_install_pre()
{
local app_name="$1"
if ! appInstallCheckRequirements "$app_name" "$CFG_AUTHELIA_REQUIRES"; then
authelia=n
return 1
fi
}
authelia_install_post_compose()
{
local app_name="$1"
local result
result=$(copyResource "$app_name" "configuration.yml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying configuration.yml to $containers_dir$app_name/config"
result=$(copyResource "$app_name" "users_database.yml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying users_database.yml to $containers_dir$app_name/config"
local authelia_config_file="$containers_dir$app_name/config/configuration.yml"
runFileOp sed -i "s|AUTHELIA_THEME_PLACEHOLDER|$CFG_AUTHELIA_THEME|g" "$authelia_config_file"
runFileOp sed -i "s|AUTHELIA_DOMAIN_PLACEHOLDER|$domain_full|g" "$authelia_config_file"
runFileOp sed -i "s|AUTHELIA_HOST_PLACEHOLDER|$host_setup|g" "$authelia_config_file"
checkSuccess "Substituting Authelia configuration values (theme=$CFG_AUTHELIA_THEME domain=$domain_full host=$host_setup)"
local authelia_secrets_dir="$containers_dir$app_name/secrets"
runFileOp mkdir -p "$authelia_secrets_dir"
local secret_name secret_file
for secret_name in JWT_SECRET SESSION_SECRET STORAGE_ENCRYPTION_KEY; do
secret_file="$authelia_secrets_dir/$secret_name"
if [[ ! -s "$secret_file" ]]; then
openssl rand -hex 64 | runFileWrite "$secret_file"
runFileOp chmod 600 "$secret_file"
fi
done
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$authelia_secrets_dir"
checkSuccess "Generated Authelia secrets at $authelia_secrets_dir"
# Authelia's metrics block lives in configuration.yml (not the compose),
# so toggle it here. The driver already toggled docker-compose.yml.
monitoringToggleAppConfig "$app_name" "config/configuration.yml"
}
authelia_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Configuring Authelia admin account"
echo ""
local authelia_admin_user="${CFG_AUTHELIA_ADMIN_USERNAME:-admin}"
local authelia_admin_pass="${CFG_AUTHELIA_ADMIN_PASSWORD:-authelia}"
local authelia_users_file="$containers_dir$app_name/config/users_database.yml"
local authelia_attempts=0
while ((authelia_attempts < 30)); do
if runFileOp docker exec authelia-service authelia --version >/dev/null 2>&1; then
break
fi
sleep 2
((authelia_attempts++))
done
if ((authelia_attempts >= 30)); then
isNotice "Authelia container did not become responsive in time — admin left at default (admin / authelia)."
return 0
fi
local authelia_hash
authelia_hash=$(runFileOp docker exec authelia-service authelia crypto hash generate argon2 --password "$authelia_admin_pass" 2>/dev/null \
| grep -oE '\$argon2[^[:space:]]+')
if [[ -z "$authelia_hash" ]]; then
isNotice "Could not generate Authelia password hash — admin left at default (admin / authelia)."
return 0
fi
runFileWrite "$authelia_users_file" <<EOF
---
users:
${authelia_admin_user}:
disabled: false
displayname: "Admin"
password: "${authelia_hash}"
email: ${authelia_admin_user}@${domain_full:-example.com}
groups:
- admins
EOF
runFileOp chown "$docker_install_user":"$docker_install_user" "$authelia_users_file"
isSuccessful "Configured Authelia admin (user: $authelia_admin_user)."
dockerComposeRestart "$app_name"
}
authelia_install_post()
{
local app_name="$1"
local authelia_admin_user="${CFG_AUTHELIA_ADMIN_USERNAME:-admin}"
local authelia_admin_pass="${CFG_AUTHELIA_ADMIN_PASSWORD:-authelia}"
echo ""
isNotice "Authelia admin login:"
echo ""
echo " Username : ${authelia_admin_user}"
echo " Password : ${authelia_admin_pass}"
echo ""
}

View File

@ -12,13 +12,7 @@
# ADMIN_PASSWORD = password used for the Bookstack admin account # ADMIN_PASSWORD = password used for the Bookstack admin account
# #
CFG_BOOKSTACK_APP_NAME=bookstack CFG_BOOKSTACK_APP_NAME=bookstack
# MULTI_INSTANCE = if true, this app can run as multiple isolated instances
# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on
# apps whose compose identity (container_name, Traefik routers, backup labels)
# is instance-safe — see scripts/instance/instance_create.sh.
CFG_BOOKSTACK_MULTI_INSTANCE=true
CFG_BOOKSTACK_BACKUP=true CFG_BOOKSTACK_BACKUP=true
CFG_BOOKSTACK_BACKUP_STRATEGY=auto
CFG_BOOKSTACK_COMPOSE_FILE=default CFG_BOOKSTACK_COMPOSE_FILE=default
CFG_BOOKSTACK_HEALTHCHECK=true CFG_BOOKSTACK_HEALTHCHECK=true
CFG_BOOKSTACK_AUTHELIA=false CFG_BOOKSTACK_AUTHELIA=false
@ -53,10 +47,12 @@ CFG_BOOKSTACK_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_BOOKSTACK_DOMAIN=1 CFG_BOOKSTACK_DOMAIN=1
CFG_BOOKSTACK_WHITELIST=false CFG_BOOKSTACK_WHITELIST=false
CFG_BOOKSTACK_HOST_NAME=bookstack
CFG_BOOKSTACK_NETWORK=default CFG_BOOKSTACK_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -73,7 +69,7 @@ CFG_BOOKSTACK_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_BOOKSTACK_PORT_1="bookstack-service|webui|random:80|public|tcp|false|true|true|Web Interface||bookstack" CFG_BOOKSTACK_PORT_1="bookstack-service|webui|random:80|public|tcp|false|true|true|Web Interface|"
# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user)
CFG_BOOKSTACK_AUTH_PROFILE=multi_user CFG_BOOKSTACK_AUTH_PROFILE=multi_user

180
containers/bookstack/bookstack.sh Executable file
View File

@ -0,0 +1,180 @@
#!/bin/bash
# Category : Knowledge Management
# Description : Bookstack - Wiki/Knowledge Base (c/u/s/r/i):
installBookstack()
{
local config_variables="$1"
if [[ "$bookstack" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent bookstack;
local app_name=$CFG_BOOKSTACK_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$bookstack" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$bookstack" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$bookstack" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$bookstack" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$bookstack" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
menuShowFinalMessages $app_name;
bookstack_target_email="${CFG_BOOKSTACK_ADMIN_EMAIL:-admin@admin.com}"
bookstack_target_pass="${CFG_BOOKSTACK_ADMIN_PASSWORD:-password}"
bookstack_compose_file="$containers_dir$app_name/docker-compose.yml"
bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1")
bookstack_host_port="${bookstack_port_pair%%:*}"
bookstack_probe_url="http://127.0.0.1:${bookstack_host_port}/login"
isNotice "Waiting for Bookstack to come online at ${bookstack_probe_url} ..."
isNotice "This may take up to 20 seconds, please wait..."
bookstack_attempts=0
bookstack_ready=0
while ((bookstack_attempts < 60)); do
bookstack_http_code=$(curl -sS -o /dev/null --max-time 3 -w '%{http_code}' "$bookstack_probe_url" 2>/dev/null)
if [[ "$bookstack_http_code" =~ ^(200|302)$ ]]; then
bookstack_ready=1
break
fi
sleep 2
((bookstack_attempts++))
done
if ((bookstack_ready == 0)); then
isNotice "Bookstack did not respond on ${bookstack_probe_url} within $((60 * 2))s — admin account left at upstream defaults."
echo ""
isNotice "Bookstack admin login (default):"
echo ""
echo " Email : admin@admin.com"
echo " Password : password"
echo ""
else
isSuccessful "Bookstack is online (HTTP ${bookstack_http_code})."
bookstack_create_output=$(sudo docker exec \
-e EZ_BS_NEW_EMAIL="$bookstack_target_email" \
-e EZ_BS_NEW_PASS="$bookstack_target_pass" \
bookstack sh -c 'cd /app/www && s6-setuidgid abc php artisan bookstack:create-admin --no-ansi --email="$EZ_BS_NEW_EMAIL" --name=Admin --password="$EZ_BS_NEW_PASS" 2>&1')
bookstack_create_rc=$?
if [[ $bookstack_create_rc -eq 0 ]]; then
isSuccessful "Bookstack admin account created (email: $bookstack_target_email)."
if [[ "$bookstack_target_email" != "admin@admin.com" ]]; then
sudo docker exec -i bookstack php /app/www/artisan tinker --no-ansi >/dev/null 2>&1 <<'PHP'
$c = class_exists('\BookStack\Users\Models\User') ? '\BookStack\Users\Models\User' : '\BookStack\Auth\User';
optional($c::where('email', 'admin@admin.com')->first())->delete();
PHP
isSuccessful "Removed seeded admin@admin.com account."
fi
echo ""
isNotice "Bookstack admin login:"
echo ""
echo " Email : ${bookstack_target_email}"
echo " Password : ${bookstack_target_pass}"
echo ""
else
isNotice "Bookstack admin auto-create failed (exit $bookstack_create_rc). Output:"
echo "$bookstack_create_output" | sed 's/^/ /'
echo ""
isNotice "Falling back to upstream defaults — update from inside Bookstack."
echo ""
isNotice "Bookstack admin login (default):"
echo ""
echo " Email : admin@admin.com"
echo " Password : password"
echo ""
fi
fi
menu_number=0
#sleep 3s
cd
fi
bookstack=n
}

View File

@ -32,17 +32,13 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "mariadb:bookstack_db:db:"
libreportal.backup.files: "bookstack:/config:data"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.bookstack-service.entrypoints: web,websecure traefik.http.routers.bookstack-service.entrypoints: web,websecure
traefik.http.routers.bookstack-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.bookstack-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.bookstack-service.tls: true traefik.http.routers.bookstack-service.tls: true
traefik.http.routers.bookstack-service.tls.certresolver: production traefik.http.routers.bookstack-service.tls.certresolver: production
traefik.http.services.bookstack-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.bookstack-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.bookstack-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.bookstack-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
# GLUETUN_OFF_BEGIN # GLUETUN_OFF_BEGIN
networks: networks:

View File

@ -1,84 +0,0 @@
#!/bin/bash
# Bookstack install hooks — drive the post-start admin account bootstrap.
# Generic installApp driver handles compose / start / db / monitoring; this
# adds the readiness probe + first-admin provisioning the original
# installBookstack() did inline.
bookstack_install_post_start()
{
local app_name="$1"
local bookstack_target_email="${CFG_BOOKSTACK_ADMIN_EMAIL:-admin@admin.com}"
local bookstack_target_pass="${CFG_BOOKSTACK_ADMIN_PASSWORD:-password}"
local bookstack_compose_file="$containers_dir$app_name/docker-compose.yml"
local bookstack_port_pair
bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1")
local bookstack_host_port="${bookstack_port_pair%%:*}"
local bookstack_probe_url="http://127.0.0.1:${bookstack_host_port}/login"
isNotice "Waiting for Bookstack to come online at ${bookstack_probe_url} ..."
isNotice "This may take up to 20 seconds, please wait..."
local bookstack_attempts=0
local bookstack_ready=0
local bookstack_http_code
while ((bookstack_attempts < 60)); do
bookstack_http_code=$(curl -sS -o /dev/null --max-time 3 -w '%{http_code}' "$bookstack_probe_url" 2>/dev/null)
if [[ "$bookstack_http_code" =~ ^(200|302)$ ]]; then
bookstack_ready=1
break
fi
sleep 2
((bookstack_attempts++))
done
if ((bookstack_ready == 0)); then
isNotice "Bookstack did not respond on ${bookstack_probe_url} within $((60 * 2))s — admin account left at upstream defaults."
echo ""
isNotice "Bookstack admin login (default):"
echo ""
echo " Email : admin@admin.com"
echo " Password : password"
echo ""
return 0
fi
isSuccessful "Bookstack is online (HTTP ${bookstack_http_code})."
local bookstack_create_output
bookstack_create_output=$(runFileOp docker exec \
-e EZ_BS_NEW_EMAIL="$bookstack_target_email" \
-e EZ_BS_NEW_PASS="$bookstack_target_pass" \
bookstack sh -c 'cd /app/www && s6-setuidgid abc php artisan bookstack:create-admin --no-ansi --email="$EZ_BS_NEW_EMAIL" --name=Admin --password="$EZ_BS_NEW_PASS" 2>&1')
local bookstack_create_rc=$?
if [[ $bookstack_create_rc -eq 0 ]]; then
isSuccessful "Bookstack admin account created (email: $bookstack_target_email)."
if [[ "$bookstack_target_email" != "admin@admin.com" ]]; then
runFileOp docker exec -i bookstack php /app/www/artisan tinker --no-ansi >/dev/null 2>&1 <<'PHP'
$c = class_exists('\BookStack\Users\Models\User') ? '\BookStack\Users\Models\User' : '\BookStack\Auth\User';
optional($c::where('email', 'admin@admin.com')->first())->delete();
PHP
isSuccessful "Removed seeded admin@admin.com account."
fi
echo ""
isNotice "Bookstack admin login:"
echo ""
echo " Email : ${bookstack_target_email}"
echo " Password : ${bookstack_target_pass}"
echo ""
else
isNotice "Bookstack admin auto-create failed (exit $bookstack_create_rc). Output:"
echo "$bookstack_create_output" | sed 's/^/ /'
echo ""
isNotice "Falling back to upstream defaults — update from inside Bookstack."
echo ""
isNotice "Bookstack admin login (default):"
echo ""
echo " Email : admin@admin.com"
echo " Password : password"
echo ""
fi
}

View File

@ -1,106 +0,0 @@
{
"tools": [
{
"id": "reset_password",
"category": "users",
"label": "Reset User Password",
"description": "Set a new password for an existing user. Leave blank to generate one.",
"icon": "🔑",
"fields": [
{
"name": "email",
"label": "User email",
"type": "text",
"placeholder": "user@example.com",
"required": true
},
{
"name": "password",
"label": "New password",
"type": "password",
"placeholder": "Leave blank to generate"
}
]
},
{
"id": "create_account",
"category": "users",
"label": "Create User Account",
"description": "Add a new user. Tick \"Make admin\" for full rights.",
"icon": "👤",
"fields": [
{
"name": "email",
"label": "Email",
"type": "text",
"placeholder": "user@example.com",
"required": true
},
{
"name": "name",
"label": "Display name",
"type": "text",
"required": true
},
{
"name": "password",
"label": "Password",
"type": "password",
"placeholder": "Leave blank to generate"
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
},
{
"id": "list_users",
"category": "users",
"label": "List Users",
"description": "List every user and their role.",
"icon": "📋",
"fields": []
},
{
"id": "delete_user",
"category": "users",
"label": "Delete User Account",
"description": "Permanently remove a user.",
"icon": "🗑",
"destructive": true,
"confirm": "This cannot be undone.",
"fields": [
{
"name": "email",
"label": "User email",
"type": "text",
"required": true
}
]
},
{
"id": "set_admin",
"category": "users",
"label": "Set Admin Status",
"description": "Promote or demote a user.",
"icon": "👑",
"fields": [
{
"name": "email",
"label": "User email",
"type": "text",
"required": true
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
}
]
}

View File

@ -19,7 +19,6 @@ CFG_CROWDSEC_HOST_SERVICE=crowdsec
CFG_CROWDSEC_HOST_SERVICES=crowdsec.service,crowdsec-firewall-bouncer.service CFG_CROWDSEC_HOST_SERVICES=crowdsec.service,crowdsec-firewall-bouncer.service
CFG_CROWDSEC_HOST_LOG_FILES="crowdsec.service|/var/log/crowdsec.log,crowdsec-firewall-bouncer.service|/var/log/crowdsec-firewall-bouncer.log" CFG_CROWDSEC_HOST_LOG_FILES="crowdsec.service|/var/log/crowdsec.log,crowdsec-firewall-bouncer.service|/var/log/crowdsec-firewall-bouncer.log"
CFG_CROWDSEC_BACKUP=true CFG_CROWDSEC_BACKUP=true
CFG_CROWDSEC_BACKUP_STRATEGY=auto
CFG_CROWDSEC_MONITORING=false CFG_CROWDSEC_MONITORING=false
CFG_CROWDSEC_PROMETHEUS_LISTEN=0.0.0.0:6060 CFG_CROWDSEC_PROMETHEUS_LISTEN=0.0.0.0:6060
# #

View File

@ -4,8 +4,8 @@
# Description : CrowdSec - Intrusion Prevention (c/u/s/r/i): # Description : CrowdSec - Intrusion Prevention (c/u/s/r/i):
# #
# Host-installed agent (apt + systemd) — no Docker container. Host install # Host-installed agent (apt + systemd) — no Docker container. Host install
# logic lives in scripts/crowdsec_install_host.sh (installCrowdsecHost) beside # logic lives in scripts/install/install_crowdsec.sh (installCrowdsecHost);
# this file; install registration uses the shared hostAppInstall helper # install registration uses the shared hostAppInstall helper
# (scripts/install/host_app.sh). uninstall/stop/restartCrowdsec (below) are the # (scripts/install/host_app.sh). uninstall/stop/restartCrowdsec (below) are the
# host-side hooks dockerUninstallApp / dockerStopApp / dockerRestartApp invoke. # host-side hooks dockerUninstallApp / dockerStopApp / dockerRestartApp invoke.
@ -58,18 +58,18 @@ uninstallCrowdsec()
echo "" echo ""
echo "---- $menu_number. Stopping CrowdSec host services." echo "---- $menu_number. Stopping CrowdSec host services."
echo "" echo ""
local result; result=$(runSystem systemctl disable --now crowdsec-firewall-bouncer 2>&1) local result=$(sudo systemctl disable --now crowdsec-firewall-bouncer 2>&1)
checkSuccess "Disabling firewall bouncer" checkSuccess "Disabling firewall bouncer"
local result; result=$(runSystem systemctl disable --now crowdsec 2>&1) local result=$(sudo systemctl disable --now crowdsec 2>&1)
checkSuccess "Disabling agent" checkSuccess "Disabling agent"
((menu_number++)) ((menu_number++))
echo "" echo ""
echo "---- $menu_number. Removing CrowdSec packages." echo "---- $menu_number. Removing CrowdSec packages."
echo "" echo ""
local result; result=$(runSystem DEBIAN_FRONTEND=noninteractive apt-get purge -y -q crowdsec crowdsec-firewall-bouncer-nftables </dev/null 2>&1) local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -q crowdsec crowdsec-firewall-bouncer-nftables </dev/null 2>&1)
checkSuccess "Purged packages" checkSuccess "Purged packages"
local result; result=$(runSystem DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -q </dev/null 2>&1) local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -q </dev/null 2>&1)
checkSuccess "Removed orphaned dependencies" checkSuccess "Removed orphaned dependencies"
crowdsecToggleLibrePortalLogMounts off crowdsecToggleLibrePortalLogMounts off
@ -81,9 +81,9 @@ uninstallCrowdsec()
stopCrowdsec() stopCrowdsec()
{ {
isNotice "Stopping CrowdSec host services..." isNotice "Stopping CrowdSec host services..."
local result; result=$(runSystem systemctl stop crowdsec-firewall-bouncer 2>&1) local result=$(sudo systemctl stop crowdsec-firewall-bouncer 2>&1)
checkSuccess "Stopped firewall bouncer" checkSuccess "Stopped firewall bouncer"
local result; result=$(runSystem systemctl stop crowdsec 2>&1) local result=$(sudo systemctl stop crowdsec 2>&1)
checkSuccess "Stopped agent" checkSuccess "Stopped agent"
} }
@ -93,8 +93,8 @@ stopCrowdsec()
restartCrowdsec() restartCrowdsec()
{ {
isNotice "Restarting CrowdSec host services..." isNotice "Restarting CrowdSec host services..."
local result; result=$(runSystem systemctl restart crowdsec 2>&1) local result=$(sudo systemctl restart crowdsec 2>&1)
checkSuccess "Restarted agent" checkSuccess "Restarted agent"
local result; result=$(runSystem systemctl restart crowdsec-firewall-bouncer 2>&1) local result=$(sudo systemctl restart crowdsec-firewall-bouncer 2>&1)
checkSuccess "Restarted firewall bouncer" checkSuccess "Restarted firewall bouncer"
} }

View File

@ -1,19 +0,0 @@
#!/bin/bash
appCrowdSecFixPriority() {
local cfg="/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml"
if [[ ! -f "$cfg" ]]; then
isNotice "Bouncer config not found at $cfg — is CrowdSec installed?"
return 1
fi
# The bouncer yaml is root-owned under /etc/crowdsec; the backup + nftables
# ipv4/ipv6 priority rewrite (to -100) runs in the root-owned crowdsec helper.
runCrowdsec bouncer-priority
checkSuccess "Patched nftables priority to -100 in $cfg"
runSystem systemctl restart crowdsec-firewall-bouncer
checkSuccess "Restarted crowdsec-firewall-bouncer"
isSuccessful "Priority updated. Run 'crowdsec_verify_firewall' to confirm CrowdSec now runs before UFW."
}

View File

@ -11,7 +11,6 @@
# #
CFG_DASHY_APP_NAME=dashy CFG_DASHY_APP_NAME=dashy
CFG_DASHY_BACKUP=true CFG_DASHY_BACKUP=true
CFG_DASHY_BACKUP_STRATEGY=auto
CFG_DASHY_COMPOSE_FILE=default CFG_DASHY_COMPOSE_FILE=default
CFG_DASHY_HEALTHCHECK=true CFG_DASHY_HEALTHCHECK=true
CFG_DASHY_AUTHELIA=false CFG_DASHY_AUTHELIA=false
@ -38,10 +37,12 @@ CFG_DASHY_ACTIONS="configure|install|restart|shutdown|uninstall|tools"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_DASHY_DOMAIN=1 CFG_DASHY_DOMAIN=1
CFG_DASHY_WHITELIST=false CFG_DASHY_WHITELIST=false
CFG_DASHY_HOST_NAME=dashy
CFG_DASHY_NETWORK=default CFG_DASHY_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -58,7 +59,7 @@ CFG_DASHY_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_DASHY_PORT_1="dashy-service|webui|random:8080|private|tcp|false|false|true|Dashboard||dashy" CFG_DASHY_PORT_1="dashy-service|webui|random:8080|private|tcp|false|false|true|Dashboard|"
# Comma-separated list of installed app slugs to surface as shortcuts on # Comma-separated list of installed app slugs to surface as shortcuts on
# the dashy dashboard. Managed via the Tools tab → "Manage Shortcuts". # the dashy dashboard. Managed via the Tools tab → "Manage Shortcuts".
# Empty = no app shortcuts (only the static page header survives). # Empty = no app shortcuts (only the static page header survives).

114
containers/dashy/dashy.sh Executable file
View File

@ -0,0 +1,114 @@
#!/bin/bash
# Category : Miscellaneous
# Description : Dashy - Dashboard Tool (c/t/u/s/r/i):
installDashy()
{
local config_variables="$1"
if [[ "$dashy" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent dashy;
local app_name=$CFG_DASHY_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$dashy" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$dashy" == *[tT]* ]]; then
dashyToolsMenu;
fi
if [[ "$dashy" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$dashy" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$dashy" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$dashy" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your new service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
dashy=n
}

View File

@ -28,14 +28,12 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.dashy-service.entrypoints: web,websecure traefik.http.routers.dashy-service.entrypoints: web,websecure
traefik.http.routers.dashy-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.dashy-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.dashy-service.tls: true traefik.http.routers.dashy-service.tls: true
traefik.http.routers.dashy-service.tls.certresolver: production traefik.http.routers.dashy-service.tls.certresolver: production
traefik.http.services.dashy-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.dashy-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.dashy-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.dashy-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
# GLUETUN_OFF_BEGIN # GLUETUN_OFF_BEGIN
networks: networks:

View File

@ -1,10 +0,0 @@
#!/bin/bash
# Post-install/update specifics for Dashy — dispatched by appUpdateSpecifics.
appUpdateSpecifics_dashy() {
# Refresh apps-services.json (the source of truth appDashyUpdateConf reads)
# before generating dashy's conf.yml. On a first dashy install the file may
# not yet reflect dashy itself; on a re-install the previous selection survives.
webuiLibrePortalUpdate
appDashyUpdateConf
}

View File

@ -1,21 +0,0 @@
{
"tools": [
{
"id": "manage_shortcuts",
"label": "Manage Shortcuts",
"description": "Pick which apps appear on the Dashy dashboard.",
"icon": "🧩",
"fields": [
{
"name": "selected",
"label": "Apps to show on the dashboard",
"type": "app_urls_multi",
"prefillFromCfgKey": "CFG_DASHY_SHORTCUTS",
"excludeApps": [
"dashy"
]
}
]
}
]
}

View File

@ -11,7 +11,7 @@ services:
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1 - "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
# GLUETUN_OFF_END # GLUETUN_OFF_END
volumes: volumes:
- ./data:/opt/focalboard/data - ./data:/data
environment: environment:
- VIRTUAL_HOST:DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA - VIRTUAL_HOST:DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
- VIRTUAL_PORT:8000 - VIRTUAL_PORT:8000
@ -22,17 +22,13 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "sqlite:::data/focalboard.db"
libreportal.backup.files: "focalboard-service:/opt/focalboard/data:data"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.focalboard-service.entrypoints: web,websecure traefik.http.routers.focalboard-service.entrypoints: web,websecure
traefik.http.routers.focalboard-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.focalboard-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.focalboard-service.tls: true traefik.http.routers.focalboard-service.tls: true
traefik.http.routers.focalboard-service.tls.certresolver: production traefik.http.routers.focalboard-service.tls.certresolver: production
traefik.http.services.focalboard-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.focalboard-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.focalboard-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.focalboard-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
# GLUETUN_OFF_BEGIN # GLUETUN_OFF_BEGIN
networks: networks:

View File

@ -11,7 +11,6 @@
# #
CFG_FOCALBOARD_APP_NAME=focalboard CFG_FOCALBOARD_APP_NAME=focalboard
CFG_FOCALBOARD_BACKUP=true CFG_FOCALBOARD_BACKUP=true
CFG_FOCALBOARD_BACKUP_STRATEGY=auto
CFG_FOCALBOARD_COMPOSE_FILE=default CFG_FOCALBOARD_COMPOSE_FILE=default
CFG_FOCALBOARD_HEALTHCHECK=true CFG_FOCALBOARD_HEALTHCHECK=true
CFG_FOCALBOARD_AUTHELIA=false CFG_FOCALBOARD_AUTHELIA=false
@ -38,10 +37,12 @@ CFG_FOCALBOARD_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_FOCALBOARD_DOMAIN=1 CFG_FOCALBOARD_DOMAIN=1
CFG_FOCALBOARD_WHITELIST=false CFG_FOCALBOARD_WHITELIST=false
CFG_FOCALBOARD_HOST_NAME=board
CFG_FOCALBOARD_NETWORK=default CFG_FOCALBOARD_NETWORK=default
# ============================================================================= # =============================================================================
@ -58,7 +59,7 @@ CFG_FOCALBOARD_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_FOCALBOARD_PORT_1="focalboard-service|webui|random:8000|public|tcp|false|true|true|Web Interface||board" CFG_FOCALBOARD_PORT_1="focalboard-service|webui|random:8000|public|tcp|false|true|true|Web Interface|"
# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user)
CFG_FOCALBOARD_AUTH_PROFILE=multi_user CFG_FOCALBOARD_AUTH_PROFILE=multi_user

View File

@ -0,0 +1,110 @@
#!/bin/bash
# Category : Productivity
# Description : Focalboard - Project Management (c/u/s/r/i):
installFocalboard()
{
local config_variables="$1"
if [[ "$focalboard" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent focalboard;
local app_name=$CFG_FOCALBOARD_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$focalboard" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$focalboard" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$focalboard" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$focalboard" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$focalboard" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
focalboard=n
}

View File

@ -1,12 +0,0 @@
#!/bin/bash
# Post-install/update specifics for Focalboard — dispatched by appUpdateSpecifics.
appUpdateSpecifics_focalboard() {
local app_name="$1"
# Focalboard runs as nobody (65534) and writes its sqlite db + uploads under
# its mounted data dir; fixPermissionsBeforeStart hands the dir to the install
# user, so give it to 65534 here or the server can't open the database.
# Setting shouldrestart (not local) requests the restart in appUpdateSpecifics.
runOwnership app-data-nobody "$app_name"
shouldrestart="true"
}

View File

@ -1,83 +0,0 @@
{
"tools": [
{
"id": "reset_password",
"category": "users",
"label": "Reset User Password",
"description": "Set a new password for an existing user. Leave blank to generate one.",
"icon": "🔑",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
},
{
"name": "password",
"label": "New password",
"type": "password",
"placeholder": "Leave blank to generate"
}
]
},
{
"id": "create_account",
"category": "users",
"label": "Create User Account",
"description": "Add a new user. Tick \"Make admin\" for full rights.",
"icon": "👤",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
},
{
"name": "email",
"label": "Email",
"type": "text",
"required": true
},
{
"name": "password",
"label": "Password",
"type": "password",
"placeholder": "Leave blank to generate"
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
},
{
"id": "list_users",
"category": "users",
"label": "List Users",
"description": "List every user.",
"icon": "📋",
"fields": []
},
{
"id": "delete_user",
"category": "users",
"label": "Delete User Account",
"description": "Permanently remove a user.",
"icon": "🗑",
"destructive": true,
"confirm": "This cannot be undone.",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
}
]
}
]
}

View File

@ -64,17 +64,13 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "sqlite:::data/gitea/gitea/gitea.db"
libreportal.backup.files: "gitea-service:/data:data/gitea"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.gitea-service.entrypoints: web,websecure traefik.http.routers.gitea-service.entrypoints: web,websecure
traefik.http.routers.gitea-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.gitea-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.gitea.tls: true traefik.http.routers.gitea.tls: true
traefik.http.routers.gitea.tls.certresolver: production traefik.http.routers.gitea.tls.certresolver: production
traefik.http.services.gitea.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.gitea.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.gitea.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.gitea.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
gitea-cache: #LIBREPORTAL|SERVICE_TAG_2|gitea-cache gitea-cache: #LIBREPORTAL|SERVICE_TAG_2|gitea-cache

View File

@ -13,7 +13,6 @@
# #
CFG_GITEA_APP_NAME=gitea CFG_GITEA_APP_NAME=gitea
CFG_GITEA_BACKUP=true CFG_GITEA_BACKUP=true
CFG_GITEA_BACKUP_STRATEGY=auto
CFG_GITEA_COMPOSE_FILE=default CFG_GITEA_COMPOSE_FILE=default
CFG_GITEA_HEALTHCHECK=true CFG_GITEA_HEALTHCHECK=true
CFG_GITEA_AUTHELIA=false CFG_GITEA_AUTHELIA=false
@ -42,10 +41,12 @@ CFG_GITEA_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_GITEA_DOMAIN=1 CFG_GITEA_DOMAIN=1
CFG_GITEA_WHITELIST=false CFG_GITEA_WHITELIST=false
CFG_GITEA_HOST_NAME=gitea
CFG_GITEA_NETWORK=default CFG_GITEA_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -62,7 +63,7 @@ CFG_GITEA_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_GITEA_PORT_1="gitea-service|webui|random:3000|public|tcp|false|true|true|Web Interface||gitea" CFG_GITEA_PORT_1="gitea-service|webui|random:3000|public|tcp|false|true|true|Web Interface|"
CFG_GITEA_PORT_2="gitea-service|ssh|random:22|private|tcp|false|false|false|Git SSH Access|" CFG_GITEA_PORT_2="gitea-service|ssh|random:22|private|tcp|false|false|false|Git SSH Access|"
# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user)

139
containers/gitea/gitea.sh Executable file
View File

@ -0,0 +1,139 @@
#!/bin/bash
# Category : Development & Version Control
# Description : Gitea - Git Repository Management (c/u/s/r/i):
installGitea()
{
local config_variables="$1"
if [[ "$gitea" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent gitea;
local app_name=$CFG_GITEA_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$gitea" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$gitea" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$gitea" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$gitea" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$gitea" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
# Enable Gitea's /metrics endpoint only when CFG_GITEA_MONITORING=true
# (toggles the libreportal-monitoring marker block in the compose).
monitoringToggleAppConfig "$app_name" "docker-compose.yml";
# /metrics rides Gitea's public web port, so it's locked behind a
# bearer token. CFG_GITEA_METRICS_TOKEN lives in the .config (filled
# once by the RANDOMIZEDPASSWORD scanner, preserved across reinstalls)
# and reaches the compose via the GITEA_METRICS_TOKEN_TAG tag — mirror
# that same value into the Prometheus scrape fragment so the two agree.
if monitoringAppEnabled "$app_name"; then
if [[ -n "$CFG_GITEA_METRICS_TOKEN" ]]; then
sudo sed -i "s|GITEA_METRICS_TOKEN_PLACEHOLDER|${CFG_GITEA_METRICS_TOKEN}|g" \
"$containers_dir$app_name/resources/monitoring/prometheus-scrape.yml"
checkSuccess "Synced Gitea /metrics token to the Prometheus scrape config"
else
isNotice "CFG_GITEA_METRICS_TOKEN is empty — Gitea /metrics scrape may 401."
fi
fi
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing monitoring integration."
echo ""
# Self-correcting: adds Gitea's scrape target + dashboard to
# Prometheus/Grafana when CFG_GITEA_MONITORING=true, removes them when
# it's off. No-ops with a notice if either app isn't installed.
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
gitea=n
}

View File

@ -1,25 +0,0 @@
#!/bin/bash
# Gitea install hooks — mirror CFG_GITEA_METRICS_TOKEN into the Prometheus
# scrape fragment so the bearer token in the compose env matches what the
# Prometheus side sends.
gitea_install_post_compose()
{
local app_name="$1"
# The driver already ran monitoringToggleAppConfig "$app_name" docker-compose.yml,
# so the metrics block reflects CFG_GITEA_MONITORING. /metrics rides
# Gitea's public web port and is token-protected; sync the token into
# the scrape config so the two sides agree.
if monitoringAppEnabled "$app_name"; then
if [[ -n "$CFG_GITEA_METRICS_TOKEN" ]]; then
local result
result=$(runFileOp sed -i "s|GITEA_METRICS_TOKEN_PLACEHOLDER|${CFG_GITEA_METRICS_TOKEN}|g" \
"$containers_dir$app_name/resources/monitoring/prometheus-scrape.yml")
checkSuccess "Synced Gitea /metrics token to the Prometheus scrape config"
else
isNotice "CFG_GITEA_METRICS_TOKEN is empty — Gitea /metrics scrape may 401."
fi
fi
}

View File

@ -1,105 +0,0 @@
{
"tools": [
{
"id": "reset_password",
"category": "users",
"label": "Reset User Password",
"description": "Set a new password for an existing user. Leave blank to generate one.",
"icon": "🔑",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
},
{
"name": "password",
"label": "New password",
"type": "password",
"placeholder": "Leave blank to generate"
}
]
},
{
"id": "create_account",
"category": "users",
"label": "Create User Account",
"description": "Add a new user. Tick \"Make admin\" for full rights.",
"icon": "👤",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
},
{
"name": "email",
"label": "Email",
"type": "text",
"placeholder": "user@example.com",
"required": true
},
{
"name": "password",
"label": "Password",
"type": "password",
"placeholder": "Leave blank to generate"
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
},
{
"id": "list_users",
"category": "users",
"label": "List Users",
"description": "List every user.",
"icon": "📋",
"fields": []
},
{
"id": "delete_user",
"category": "users",
"label": "Delete User Account",
"description": "Permanently remove a user.",
"icon": "🗑",
"destructive": true,
"confirm": "This cannot be undone.",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
}
]
},
{
"id": "set_admin",
"category": "users",
"label": "Set Admin Status",
"description": "Promote or demote a user.",
"icon": "👑",
"fields": [
{
"name": "username",
"label": "Username",
"type": "text",
"required": true
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
}
]
}

View File

@ -12,7 +12,6 @@
# #
CFG_GLUETUN_APP_NAME=gluetun CFG_GLUETUN_APP_NAME=gluetun
CFG_GLUETUN_BACKUP=true CFG_GLUETUN_BACKUP=true
CFG_GLUETUN_BACKUP_STRATEGY=auto
CFG_GLUETUN_COMPOSE_FILE=default CFG_GLUETUN_COMPOSE_FILE=default
CFG_GLUETUN_HEALTHCHECK=true CFG_GLUETUN_HEALTHCHECK=true
CFG_GLUETUN_AUTHELIA=false CFG_GLUETUN_AUTHELIA=false
@ -71,10 +70,12 @@ CFG_GLUETUN_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips on traefik, if false allow all # WHITELIST = if true only allow whitelisted ips on traefik, if false allow all
# #
CFG_GLUETUN_DOMAIN=1 CFG_GLUETUN_DOMAIN=1
CFG_GLUETUN_WHITELIST=false CFG_GLUETUN_WHITELIST=false
CFG_GLUETUN_HOST_NAME=gluetun
CFG_GLUETUN_NETWORK=default CFG_GLUETUN_NETWORK=default
# #
# ============================================================================= # =============================================================================

View File

@ -0,0 +1,136 @@
#!/bin/bash
# Category : Networking
# Description : Gluetun - VPN client for routing other containers (c/u/s/r/i):
installGluetun()
{
local config_variables="$1"
if [[ "$gluetun" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent gluetun;
local app_name=$CFG_GLUETUN_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$gluetun" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$gluetun" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$gluetun" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$gluetun" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$gluetun" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
monitoringToggleAppConfig "$app_name" "docker-compose.yml";
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating the WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing Gluetun provider snapshot."
echo ""
webuiGenerateGluetunProviders;
((menu_number++))
echo ""
echo "---- $menu_number. Re-attaching gluetun-routed apps (post-recreate)."
echo ""
# Gluetun was just (re)created — every existing routed app holds a
# stale container ID in its network_mode. Reattach them now so the
# user doesn't have to chase silent netns drift later.
appGluetunRoutedRecreate
((menu_number++))
echo ""
echo "---- $menu_number. Routing existing apps through Gluetun (optional)."
echo ""
gluetunRouteExistingAppsPrompt;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing monitoring integration."
echo ""
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
menuShowFinalMessages "$app_name";
menu_number=0
cd
fi
gluetun=n
}

View File

@ -1,16 +0,0 @@
#!/bin/bash
# App-specific compose tags for Gluetun (VPN gateway) + its forwarded-port wiring.
appSetupComposeTags_gluetun() {
local full_file_path="$1"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_VPN_SERVICE_PROVIDER_TAG" "$CFG_GLUETUN_VPN_SERVICE_PROVIDER"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_VPN_TYPE_TAG" "$CFG_GLUETUN_VPN_TYPE"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_VPN_COUNTRIES_TAG" "$CFG_GLUETUN_VPN_COUNTRIES"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_OPENVPN_USER_TAG" "$CFG_GLUETUN_OPENVPN_USER"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_OPENVPN_PASSWORD_TAG" "$CFG_GLUETUN_OPENVPN_PASSWORD"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_WIREGUARD_PRIVATE_KEY_TAG" "$CFG_GLUETUN_WIREGUARD_PRIVATE_KEY"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_WIREGUARD_ADDRESSES_TAG" "$CFG_GLUETUN_WIREGUARD_ADDRESSES"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_HEALTH_TARGETS_TAG" "${CFG_GLUETUN_HEALTH_TARGETS:-mullvad.net:443,eff.org:443}"
tagsManagerUpdateUniversalTag "$full_file_path" "GLUETUN_HEALTH_ICMP_IPS_TAG" "${CFG_GLUETUN_HEALTH_ICMP_IPS:-9.9.9.9}"
appNetworkRegisterPorts_gluetun
}

View File

@ -1,35 +0,0 @@
#!/bin/bash
# Gluetun install hooks — post-start provider snapshot refresh + reattach
# any apps routed through gluetun (their network_mode holds a stale
# container ID after gluetun was just recreated) + offer to onboard
# existing apps.
gluetun_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing Gluetun provider snapshot."
echo ""
appWebuiRefresh_gluetun
((menu_number++))
echo ""
echo "---- $menu_number. Re-attaching gluetun-routed apps (post-recreate)."
echo ""
# Gluetun was just (re)created — every existing routed app holds a
# stale container ID in its network_mode. Reattach them now so the
# user doesn't have to chase silent netns drift later.
appGluetunRoutedRecreate
((menu_number++))
echo ""
echo "---- $menu_number. Routing existing apps through Gluetun (optional)."
echo ""
gluetunRouteExistingAppsPrompt
}

View File

@ -1,11 +0,0 @@
{
"tools": [
{
"id": "refresh_providers",
"label": "Refresh VPN Providers",
"description": "Refresh the VPN provider and country lists.",
"icon": "🔄",
"fields": []
}
]
}

View File

@ -24,14 +24,12 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.grafana-service.entrypoints: web,websecure traefik.http.routers.grafana-service.entrypoints: web,websecure
traefik.http.routers.grafana-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.grafana-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.grafana-service.tls: true traefik.http.routers.grafana-service.tls: true
traefik.http.routers.grafana-service.tls.certresolver: production traefik.http.routers.grafana-service.tls.certresolver: production
traefik.http.services.grafana-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.grafana-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.grafana-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.grafana-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA

View File

@ -14,7 +14,6 @@
CFG_GRAFANA_APP_NAME=grafana CFG_GRAFANA_APP_NAME=grafana
CFG_GRAFANA_REQUIRES="prometheus" CFG_GRAFANA_REQUIRES="prometheus"
CFG_GRAFANA_BACKUP=true CFG_GRAFANA_BACKUP=true
CFG_GRAFANA_BACKUP_STRATEGY=auto
CFG_GRAFANA_COMPOSE_FILE=default CFG_GRAFANA_COMPOSE_FILE=default
CFG_GRAFANA_HEALTHCHECK=true CFG_GRAFANA_HEALTHCHECK=true
CFG_GRAFANA_AUTHELIA=false CFG_GRAFANA_AUTHELIA=false
@ -43,10 +42,12 @@ CFG_GRAFANA_REQUIRES_SERVICE=prometheus
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_GRAFANA_DOMAIN=1 CFG_GRAFANA_DOMAIN=1
CFG_GRAFANA_WHITELIST=false CFG_GRAFANA_WHITELIST=false
CFG_GRAFANA_HOST_NAME=grafana
CFG_GRAFANA_NETWORK=default CFG_GRAFANA_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -63,4 +64,4 @@ CFG_GRAFANA_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_GRAFANA_PORT_1="grafana-service|webui|random:3000|public|tcp|false|true|true|Web Interface||grafana" CFG_GRAFANA_PORT_1="grafana-service|webui|random:3000|public|tcp|false|true|true|Web Interface|"

133
containers/grafana/grafana.sh Executable file
View File

@ -0,0 +1,133 @@
#!/bin/bash
# Category : Development & Version Control
# Description : Grafana - Metrics Visualizer (c/u/s/r/i):
installGrafana()
{
local config_variables="$1"
if [[ "$grafana" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent grafana;
local app_name=$CFG_GRAFANA_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$grafana" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$grafana" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$grafana" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$grafana" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$grafana" == *[iI]* ]]; then
isHeader "Install $app_name"
if ! appInstallCheckRequirements "$app_name" "$CFG_GRAFANA_REQUIRES"; then
grafana=n
return 1
fi
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
# Grafana
if [ -d "${containers_dir}grafana/grafana_storage" ]; then
local result=$(sudo chmod -R 777 "${containers_dir}grafana/grafana_storage")
checkSuccess "Set permissions to grafana_storage folder."
fi
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Provisioning monitoring dashboards from installed apps."
echo ""
# Re-gather the Prometheus datasource + every monitoring-enabled app's
# dashboards into provisioning/ — so a fresh (or re-)install of Grafana
# picks up the apps that already had CFG_<APP>_MONITORING=true.
# monitoringRefreshAll also covers Grafana's own scrape target when
# CFG_GRAFANA_MONITORING=true.
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
grafana=n
}

View File

@ -1,23 +0,0 @@
#!/bin/bash
# Grafana install hooks — pre-flight prereq check + post-start 0777 on the
# storage folder so Grafana can write its sqlite db regardless of host UID.
grafana_install_pre()
{
local app_name="$1"
if ! appInstallCheckRequirements "$app_name" "$CFG_GRAFANA_REQUIRES"; then
grafana=n
return 1
fi
}
grafana_install_post_start()
{
local app_name="$1"
if [ -d "${containers_dir}grafana/grafana_storage" ]; then
local result
result=$(runFileOp chmod -R 777 "${containers_dir}grafana/grafana_storage")
checkSuccess "Set permissions to grafana_storage folder."
fi
}

View File

@ -20,8 +20,6 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "sqlite:::data/db.sqlite"
libreportal.backup.files: "headscale-service:/var/lib/headscale:data"
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN # GLUETUN_OFF_BEGIN
@ -58,11 +56,9 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_2_BEGIN traefik.http.routers.headscale-webui-service.rule: Host(`admin.DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.headscale-webui-service.rule: Host(`DOMAINSUBNAME_DATA_2`) #LIBREPORTAL|DOMAINSUBNAME_TAG_2|DOMAINSUBNAME_DATA_2
traefik.http.services.headscale-webui-service.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2 traefik.http.services.headscale-webui-service.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2
traefik.http.routers.headscale-webui-service.middlewares: MIDDLEWARE_DATA_2 #LIBREPORTAL|MIDDLEWARE_TAG_2|MIDDLEWARE_DATA_2 traefik.http.routers.headscale-webui-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_2_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ports: ports:
- "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2 - "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2

View File

@ -11,7 +11,6 @@
# #
CFG_HEADSCALE_APP_NAME=headscale CFG_HEADSCALE_APP_NAME=headscale
CFG_HEADSCALE_BACKUP=true CFG_HEADSCALE_BACKUP=true
CFG_HEADSCALE_BACKUP_STRATEGY=auto
CFG_HEADSCALE_COMPOSE_FILE=default CFG_HEADSCALE_COMPOSE_FILE=default
CFG_HEADSCALE_HEALTHCHECK=true CFG_HEADSCALE_HEALTHCHECK=true
CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1 CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1
@ -38,10 +37,12 @@ CFG_HEADSCALE_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_HEADSCALE_DOMAIN=1 CFG_HEADSCALE_DOMAIN=1
CFG_HEADSCALE_WHITELIST=false CFG_HEADSCALE_WHITELIST=false
CFG_HEADSCALE_HOST_NAME=headscale
CFG_HEADSCALE_NETWORK=default CFG_HEADSCALE_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -59,4 +60,4 @@ CFG_HEADSCALE_NETWORK=default
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_HEADSCALE_PORT_1="headscale-service|api|random:8080|private|tcp|false|false|false|Headscale API Server|" CFG_HEADSCALE_PORT_1="headscale-service|api|random:8080|private|tcp|false|false|false|Headscale API Server|"
CFG_HEADSCALE_PORT_2="headscale-webui-service|webui|random:5000|private|tcp|false|true|true|Web UI||admin.headscale" CFG_HEADSCALE_PORT_2="headscale-webui-service|webui|random:5000|private|tcp|false|true|true|Web UI|"

127
containers/headscale/headscale.sh Executable file
View File

@ -0,0 +1,127 @@
#!/bin/bash
# Category : Networking
# Description : Self-hosted WireGuard orchestrator (c/u/s/r/i):
installHeadscale()
{
local config_variables="$1"
if [[ "$headscale" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent headscale;
local app_name=$CFG_HEADSCALE_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$headscale" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$headscale" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$headscale" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$headscale" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$headscale" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
local result=$(createFolders "loud" $docker_install_user $containers_dir$app_name/config)
checkSuccess "Create config folder"
local result=$(copyResource "$app_name" "config.yaml" "config" | sudo tee -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying config.yaml to config folder."
configSetupFileWithData $app_name "config.yaml" "config";
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Setting up database records"
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Refreshing monitoring integration."
echo ""
monitoringRefreshAll;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
echo " NOTE - The password to login in defined in the yml install file that was installed"
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
headscale=n
}

View File

@ -1,19 +0,0 @@
#!/bin/bash
# Headscale install hooks — drop the config.yaml template into the
# container's config folder before start so the daemon has its config on
# first boot.
headscale_install_post_compose()
{
local app_name="$1"
local result
result=$(createFolders "loud" $docker_install_user $containers_dir$app_name/config)
checkSuccess "Create config folder"
result=$(copyResource "$app_name" "config.yaml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying config.yaml to config folder."
configSetupFileWithData $app_name "config.yaml" "config"
}

View File

@ -1,19 +0,0 @@
#!/bin/bash
tailscaleInstallToContainer()
{
local app_name="$1"
local type="$2"
local result; result=$(createFolders "loud" $docker_install_user $containers_dir$app_name/tailscale)
checkSuccess "Creating Tailscale folder"
copyFile "loud" "${install_containers_dir}headscale/resources/tailscale.sh" "$containers_dir$app_name/tailscale/tailscale.sh" $docker_install_user | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1
if [[ "$type" != "install" ]]; then
dockerComposeRestart $app_name;
fi
dockerCommandRun "docker exec -it $app_name /usr/local/bin/tailscale.sh"
checkSuccess "Executing Tailscale installer script in the $app_name container"
}

View File

@ -42,16 +42,13 @@ services:
labels: labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "postgres:invidious-db:postgresdata:"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.invidious-service.entrypoints: web,websecure traefik.http.routers.invidious-service.entrypoints: web,websecure
traefik.http.routers.invidious-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.invidious-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.invidious-service.tls: true traefik.http.routers.invidious-service.tls: true
traefik.http.routers.invidious-service.tls.certresolver: production traefik.http.routers.invidious-service.tls.certresolver: production
traefik.http.services.invidious-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.invidious-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.invidious-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.invidious-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
# GLUETUN_OFF_BEGIN # GLUETUN_OFF_BEGIN
networks: networks:

View File

@ -11,7 +11,6 @@
# #
CFG_INVIDIOUS_APP_NAME=invidious CFG_INVIDIOUS_APP_NAME=invidious
CFG_INVIDIOUS_BACKUP=false CFG_INVIDIOUS_BACKUP=false
CFG_INVIDIOUS_BACKUP_STRATEGY=auto
CFG_INVIDIOUS_COMPOSE_FILE=default CFG_INVIDIOUS_COMPOSE_FILE=default
CFG_INVIDIOUS_HEALTHCHECK=false CFG_INVIDIOUS_HEALTHCHECK=false
CFG_INVIDIOUS_AUTHELIA=false CFG_INVIDIOUS_AUTHELIA=false
@ -41,10 +40,12 @@ CFG_INVIDIOUS_ACTIONS="configure|install|restart|shutdown|uninstall|tools"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_INVIDIOUS_DOMAIN=1 CFG_INVIDIOUS_DOMAIN=1
CFG_INVIDIOUS_WHITELIST=false CFG_INVIDIOUS_WHITELIST=false
CFG_INVIDIOUS_HOST_NAME=invidious
CFG_INVIDIOUS_NETWORK=default CFG_INVIDIOUS_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -61,7 +62,7 @@ CFG_INVIDIOUS_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_INVIDIOUS_PORT_1="invidious-service|webui|random:3000|public|tcp|false|true|true|Web Interface||invidious" CFG_INVIDIOUS_PORT_1="invidious-service|webui|random:3000|public|tcp|false|true|true|Web Interface|"
# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user)
CFG_INVIDIOUS_AUTH_PROFILE=multi_user CFG_INVIDIOUS_AUTH_PROFILE=multi_user

114
containers/invidious/invidious.sh Executable file
View File

@ -0,0 +1,114 @@
#!/bin/bash
# Category : Media & Streaming
# Description : Invidious - Privacy-focused YouTube Frontend (c/u/s/r/i/t):
installInvidious()
{
local config_variables="$1"
if [[ "$invidious" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent invidious;
local app_name=$CFG_INVIDIOUS_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$invidious" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$invidious" == *[tT]* ]]; then
invidiousToolsMenu;
fi
if [[ "$invidious" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$invidious" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$invidious" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$invidious" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
invidious=n
}

View File

@ -1,77 +0,0 @@
{
"tools": [
{
"id": "reset_password",
"category": "users",
"label": "Reset User Password",
"description": "Set a new password for an existing user. Leave blank to generate one.",
"icon": "🔑",
"fields": [
{
"name": "email",
"label": "Email",
"type": "text",
"required": true
},
{
"name": "password",
"label": "New password",
"type": "password",
"placeholder": "Leave blank to generate"
}
]
},
{
"id": "create_account",
"category": "users",
"label": "Create User Account",
"description": "Add a new user. Sign-in uses the email address.",
"icon": "👤",
"fields": [
{
"name": "email",
"label": "Email",
"type": "text",
"required": true
},
{
"name": "password",
"label": "Password",
"type": "password",
"placeholder": "Leave blank to generate"
},
{
"name": "admin",
"label": "Make admin",
"type": "checkbox",
"default": false
}
]
},
{
"id": "list_users",
"category": "users",
"label": "List Users",
"description": "List every user.",
"icon": "📋",
"fields": []
},
{
"id": "delete_user",
"category": "users",
"label": "Delete User Account",
"description": "Permanently remove a user.",
"icon": "🗑",
"destructive": true,
"confirm": "This cannot be undone.",
"fields": [
{
"name": "email",
"label": "Email",
"type": "text",
"required": true
}
]
}
]
}

View File

@ -17,14 +17,12 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.ipinfo-service.entrypoints: web,websecure traefik.http.routers.ipinfo-service.entrypoints: web,websecure
traefik.http.routers.ipinfo-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.ipinfo-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.ipinfo-service.tls: true traefik.http.routers.ipinfo-service.tls: true
traefik.http.routers.ipinfo-service.tls.certresolver: production traefik.http.routers.ipinfo-service.tls.certresolver: production
traefik.http.services.ipinfo-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.ipinfo-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.ipinfo-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.ipinfo-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA

View File

@ -11,7 +11,6 @@
# #
CFG_IPINFO_APP_NAME=ipinfo CFG_IPINFO_APP_NAME=ipinfo
CFG_IPINFO_BACKUP=false CFG_IPINFO_BACKUP=false
CFG_IPINFO_BACKUP_STRATEGY=auto
CFG_IPINFO_COMPOSE_FILE=default CFG_IPINFO_COMPOSE_FILE=default
CFG_IPINFO_HEALTHCHECK=true CFG_IPINFO_HEALTHCHECK=true
CFG_IPINFO_AUTHELIA=false CFG_IPINFO_AUTHELIA=false
@ -38,10 +37,12 @@ CFG_IPINFO_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_IPINFO_DOMAIN=1 CFG_IPINFO_DOMAIN=1
CFG_IPINFO_WHITELIST=false CFG_IPINFO_WHITELIST=false
CFG_IPINFO_HOST_NAME=ipinfo
CFG_IPINFO_NETWORK=default CFG_IPINFO_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -58,4 +59,4 @@ CFG_IPINFO_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_IPINFO_PORT_1="ipinfo-service|webui|random:8080|public|tcp|false|true|true|Web Interface||ipinfo" CFG_IPINFO_PORT_1="ipinfo-service|webui|random:8080|public|tcp|false|true|true|Web Interface|"

110
containers/ipinfo/ipinfo.sh Executable file
View File

@ -0,0 +1,110 @@
#!/bin/bash
# Category : Networking
# Description : IPinfo - IP Geolocation and Information (c/u/s/r/i):
installIpinfo()
{
local config_variables="$1"
if [[ "$ipinfo" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent ipinfo;
local app_name=$CFG_IPINFO_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$ipinfo" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$ipinfo" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$ipinfo" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$ipinfo" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$ipinfo" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your $app_name service using any of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
ipinfo=n
}

View File

@ -21,14 +21,12 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.jellyfin-service.entrypoints: web,websecure traefik.http.routers.jellyfin-service.entrypoints: web,websecure
traefik.http.routers.jellyfin-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.jellyfin-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.jellyfin-service.tls: true traefik.http.routers.jellyfin-service.tls: true
traefik.http.routers.jellyfin-service.tls.certresolver: production traefik.http.routers.jellyfin-service.tls.certresolver: production
traefik.http.services.jellyfin-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.jellyfin-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.jellyfin-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.jellyfin-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck: healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA

View File

@ -11,7 +11,6 @@
# #
CFG_JELLYFIN_APP_NAME=jellyfin CFG_JELLYFIN_APP_NAME=jellyfin
CFG_JELLYFIN_BACKUP=true CFG_JELLYFIN_BACKUP=true
CFG_JELLYFIN_BACKUP_STRATEGY=auto
CFG_JELLYFIN_COMPOSE_FILE=default CFG_JELLYFIN_COMPOSE_FILE=default
CFG_JELLYFIN_HEALTHCHECK=true CFG_JELLYFIN_HEALTHCHECK=true
CFG_JELLYFIN_AUTHELIA=false CFG_JELLYFIN_AUTHELIA=false
@ -38,10 +37,12 @@ CFG_JELLYFIN_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_JELLYFIN_DOMAIN=1 CFG_JELLYFIN_DOMAIN=1
CFG_JELLYFIN_WHITELIST=false CFG_JELLYFIN_WHITELIST=false
CFG_JELLYFIN_HOST_NAME=jellyfin
CFG_JELLYFIN_NETWORK=default CFG_JELLYFIN_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -58,4 +59,4 @@ CFG_JELLYFIN_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_JELLYFIN_PORT_1="jellyfin-service|webui|random:8096|public|tcp|false|true|true|Media Server||jellyfin" CFG_JELLYFIN_PORT_1="jellyfin-service|webui|random:8096|public|tcp|false|true|true|Media Server|"

103
containers/jellyfin/jellyfin.sh Executable file
View File

@ -0,0 +1,103 @@
#!/bin/bash
# Category : Media & Streaming
# Description : Jellyfin - Media Server (c/u/s/r/i):
installJellyfin()
{
local config_variables="$1"
if [[ "$jellyfin" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent jellyfin;
local app_name=$CFG_JELLYFIN_APP_NAME
initializeAppVariables $app_name;
fi
if [[ "$jellyfin" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$jellyfin" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$jellyfin" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$jellyfin" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$jellyfin" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Pulling a default Jellyfin docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start Jellyfin"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your new service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
jellyfin=n
}

View File

@ -23,14 +23,12 @@ services:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.jitsimeet-service.entrypoints: web,websecure traefik.http.routers.jitsimeet-service.entrypoints: web,websecure
traefik.http.routers.jitsimeet-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.jitsimeet-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
traefik.http.routers.jitsimeet-service.tls: true traefik.http.routers.jitsimeet-service.tls: true
traefik.http.routers.jitsimeet-service.tls.certresolver: production traefik.http.routers.jitsimeet-service.tls.certresolver: production
traefik.http.services.jitsimeet-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.services.jitsimeet-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.jitsimeet-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 traefik.http.routers.jitsimeet-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
networks: networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA

View File

@ -11,7 +11,6 @@
# #
CFG_JITSIMEET_APP_NAME=jitsimeet CFG_JITSIMEET_APP_NAME=jitsimeet
CFG_JITSIMEET_BACKUP=true CFG_JITSIMEET_BACKUP=true
CFG_JITSIMEET_BACKUP_STRATEGY=auto
CFG_JITSIMEET_COMPOSE_FILE=default CFG_JITSIMEET_COMPOSE_FILE=default
CFG_JITSIMEET_HEALTHCHECK=true CFG_JITSIMEET_HEALTHCHECK=true
CFG_JITSIMEET_AUTHELIA=false CFG_JITSIMEET_AUTHELIA=false
@ -38,10 +37,12 @@ CFG_JITSIMEET_ACTIONS="configure|install|restart|shutdown|uninstall"
# NETWORK CONFIGURATION # NETWORK CONFIGURATION
# ============================================================================= # =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains # DOMAIN = number of domain from the general config, useful when using multiple domains
# HOST_NAME = subdomain name e.g test is the name for test.website.com
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all # WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
# #
CFG_JITSIMEET_DOMAIN=1 CFG_JITSIMEET_DOMAIN=1
CFG_JITSIMEET_WHITELIST=false CFG_JITSIMEET_WHITELIST=false
CFG_JITSIMEET_HOST_NAME=meet
CFG_JITSIMEET_NETWORK=default CFG_JITSIMEET_NETWORK=default
# #
# ============================================================================= # =============================================================================
@ -58,6 +59,6 @@ CFG_JITSIMEET_NETWORK=default
# - webui: if true, this port serves the main web interface # - webui: if true, this port serves the main web interface
# - description: human-readable description of the service # - description: human-readable description of the service
# #
CFG_JITSIMEET_PORT_1="jitsimeet-service|webui|random:80|public|tcp|false|true|true|Web Interface||meet" CFG_JITSIMEET_PORT_1="jitsimeet-service|webui|random:80|public|tcp|false|true|true|Web Interface|"
CFG_JITSIMEET_PORT_2="jitsimeet-jvb|video-bridge|random:10000|public|udp|false|false|false|Jitsi Video Bridge (UDP)|" CFG_JITSIMEET_PORT_2="jitsimeet-jvb|video-bridge|random:10000|public|udp|false|false|false|Jitsi Video Bridge (UDP)|"
CFG_JITSIMEET_PORT_3="jitsimeet-jvb|video-tcp|random:30300|public|tcp|false|false|false|Jitsi Video Bridge (TCP)|" CFG_JITSIMEET_PORT_3="jitsimeet-jvb|video-tcp|random:30300|public|tcp|false|false|false|Jitsi Video Bridge (TCP)|"

202
containers/jitsimeet/jitsimeet.sh Executable file
View File

@ -0,0 +1,202 @@
#!/bin/bash
# Category : Communication & Collaboration Tools
# Description : Jitsi Meet - Video Conferencing *UNFINISHED* (c/u/s/r/i):
installJitsimeet()
{
local config_variables="$1"
if [[ "$jitsimeet" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent jitsimeet;
local app_name=$CFG_JITSIMEET_APP_NAME
git_url=$CFG_JITSIMEET_GIT
initializeAppVariables $app_name;
fi
if [[ "$jitsimeet" == *[cC]* ]]; then
editAppConfig $app_name;
fi
if [[ "$jitsimeet" == *[uU]* ]]; then
dockerUninstallApp $app_name;
fi
if [[ "$jitsimeet" == *[sS]* ]]; then
dockerComposeDown $app_name;
fi
if [[ "$jitsimeet" == *[rR]* ]]; then
dockerComposeRestart $app_name;
fi
if [[ "$jitsimeet" == *[iI]* ]]; then
isHeader "Install $app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config file for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables";
isSuccessful "Install folders and Config files have been setup for $app_name."
((menu_number++))
echo ""
((menu_number++))
echo ""
echo "---- $menu_number. Downloading latest GitHub release"
echo ""
latest_tag=$(git ls-remote --refs --sort="version:refname" --tags $git_url | cut -d/ -f3- | tail -n1)
echo "The latest tag is: $latest_tag"
local result=$(createFolders "loud" $docker_install_user $containers_dir$app_name)
checkSuccess "Creating $app_name container installation folder"
local result=$(cd $containers_dir$app_name && sudo rm -rf $containers_dir$app_name/$latest_tag.zip)
checkSuccess "Deleting zip file to prevent conflicts"
local result=$(createTouch $containers_dir$app_name/$latest_tag.txt $docker_install_user && echo 'Installed "$latest_tag" on "$backupDate"!' > $latest_tag.txt)
checkSuccess "Create logging txt file"
# Download files and unzip
local result=$(sudo wget -O $containers_dir$app_name/$latest_tag.zip $git_url/archive/refs/tags/$latest_tag.zip)
checkSuccess "Downloading tagged zip file from GitHub"
local result=$(sudo unzip -o $containers_dir$app_name/$latest_tag.zip -d $containers_dir$app_name)
checkSuccess "Unzip downloaded file"
local result=$(sudo mv $containers_dir$app_name/docker-jitsi-meet-$latest_tag/* $containers_dir$app_name)
checkSuccess "Moving all files from zip file to install directory"
local result=$(sudo rm -rf $containers_dir$app_name/$latest_tag.zip && sudo rm -rf $containers_dir$app_name/$latest_tag/)
checkSuccess "Removing downloaded zip file as no longer needed"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml file."
echo ""
dockerComposeSetupFile $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Setting up .env file for setup"
echo ""
dockerSetupEnvFile;
# Updating custom .env values
local result=$(sudo sed -i "s|CONFIG=~/.jitsi-meet-cfg|CONFIG=$containers_dir$app_name/.jitsi-meet-cfg|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with new install path"
local result=$(sudo sed -i "s|#PUBLIC_URL=https://meet.example.com|PUBLIC_URL=https://$host_setup|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with Public URL to $host_setup"
local result=$(sudo sed -i "s|HTTP_PORT=8000|HTTP_PORT=$usedport1|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with HTTP_PORT to $usedport1"
local result=$(sudo sed -i "s|HTTPS_PORT=8443|HTTPS_PORT=$usedport2|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with HTTP_PORT to $usedport2"
#local result=$(echo "ENABLE_HTTP_REDIRECT=1" | sudo tee -a "$containers_dir$app_name/.env")
#checkSuccess "Updating .env file with option : ENABLE_HTTP_REDIRECT"
# Values are missing from the .env by default for some reason
# https://github.com/jitsi/docker-jitsi-meet/commit/12051700562d9826f9e024ad649c4dd9b88f94de#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5
local result=$(echo "XMPP_DOMAIN=meet.jitsi" | sudo tee -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : XMPP_DOMAIN"
local result=$(echo "XMPP_SERVER=xmpp.meet.jitsi" | sudo tee -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : XMPP_SERVER"
local result=$(echo "JVB_PORT=$usedport4" | sudo tee -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_PORT"
local result=$(echo "JVB_TCP_MAPPED_PORT=$usedport5" | sudo tee -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_TCP_MAPPED_PORT"
local result=$(echo "JVB_TCP_PORT=$usedport5" | sudo tee -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_TCP_PORT"
local result=$(cd "$containers_dir$app_name" && sudo ./gen-passwords.sh)
checkSuccess "Running Jitsi Meet gen-passwords.sh script"
((menu_number++))
echo ""
echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name"
echo ""
dockerComposeUpdateAndStartApp $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. Adjusting $app_name docker system files for port changes."
echo ""
#dockerCommandRun "docker exec -it $app_name /bin/bash && cd /"
#local result=$(sudo sed -i "s|80|$usedport1|g" $containers_dir$app_nameweb/default)
#checkSuccess "Updating Docker NGINX default site port 80 to $usedport1"
#local result=$(sudo sed -i "s|443|$usedport2|g" $containers_dir$app_nameweb/default)
#checkSuccess "Updating Docker NGINX default site port 443 to $usedport2"
local result=$(sudo sed -i "s|80|$usedport1|g" $containers_dir$app_name/web/rootfs/defaults/default)
checkSuccess "Updating NGINX default site port 80 to $usedport1"
local result=$(sudo sed -i "s|443|$usedport2|g" $containers_dir$app_name/web/rootfs/defaults/default)
checkSuccess "Updating NGINX default site port 443 to $usedport2"
#dockerCommandRun "docker cp '$containers_dir$app_name' '$app_name:/etc/nginx/sites-available/default'"
dockerComposeRestart $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Application specific updates (if required)"
echo ""
appUpdateSpecifics $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Adding $app_name to the Apps Database table."
echo ""
databaseInstallApp $app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI config file."
echo ""
webuiContainerSetup $app_name install;
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
echo " You can now navigate to your new service using one of the options below : "
echo ""
menuShowFinalMessages $app_name;
menu_number=0
#sleep 3s
cd
fi
jitsimeet=n
}

View File

@ -1,101 +0,0 @@
#!/bin/bash
# Jitsi Meet install hooks — Jitsi ships its docker layout as a tagged
# release zip on GitHub, so we download + unpack it before compose setup.
# Then mass-edit the .env, generate passwords, and rewire the nginx ports.
jitsimeet_install_post_setup()
{
local app_name="$1"
local git_url="$CFG_JITSIMEET_GIT"
((menu_number++))
echo ""
echo "---- $menu_number. Downloading latest GitHub release"
echo ""
local latest_tag
latest_tag=$(git ls-remote --refs --sort="version:refname" --tags "$git_url" | cut -d/ -f3- | tail -n1)
echo "The latest tag is: $latest_tag"
local result
result=$(createFolders "loud" $docker_install_user $containers_dir$app_name)
checkSuccess "Creating $app_name container installation folder"
result=$(cd $containers_dir$app_name && runFileOp rm -rf $containers_dir$app_name/$latest_tag.zip)
checkSuccess "Deleting zip file to prevent conflicts"
result=$(createTouch $containers_dir$app_name/$latest_tag.txt $docker_install_user && echo "Installed \"$latest_tag\" on \"$backupDate\"!" > $latest_tag.txt)
checkSuccess "Create logging txt file"
result=$(runFileOp wget -O $containers_dir$app_name/$latest_tag.zip $git_url/archive/refs/tags/$latest_tag.zip)
checkSuccess "Downloading tagged zip file from GitHub"
result=$(runFileOp unzip -o $containers_dir$app_name/$latest_tag.zip -d $containers_dir$app_name)
checkSuccess "Unzip downloaded file"
result=$(runFileOp mv $containers_dir$app_name/docker-jitsi-meet-$latest_tag/* $containers_dir$app_name)
checkSuccess "Moving all files from zip file to install directory"
result=$(runFileOp rm -rf $containers_dir$app_name/$latest_tag.zip && runFileOp rm -rf $containers_dir$app_name/$latest_tag/)
checkSuccess "Removing downloaded zip file as no longer needed"
}
jitsimeet_install_post_compose()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up .env file for setup"
echo ""
dockerSetupEnvFile
local result
result=$(runFileOp sed -i "s|CONFIG=~/.jitsi-meet-cfg|CONFIG=$containers_dir$app_name/.jitsi-meet-cfg|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with new install path"
result=$(runFileOp sed -i "s|#PUBLIC_URL=https://meet.example.com|PUBLIC_URL=https://$host_setup|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with Public URL to $host_setup"
result=$(runFileOp sed -i "s|HTTP_PORT=8000|HTTP_PORT=$usedport1|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with HTTP_PORT to $usedport1"
result=$(runFileOp sed -i "s|HTTPS_PORT=8443|HTTPS_PORT=$usedport2|g" $containers_dir$app_name/.env)
checkSuccess "Updating .env file with HTTP_PORT to $usedport2"
# Defaults missing from the shipped .env (see jitsi/docker-jitsi-meet
# commit 12051700562d…). Append them here so the install boots.
result=$(echo "XMPP_DOMAIN=meet.jitsi" | runFileWrite -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : XMPP_DOMAIN"
result=$(echo "XMPP_SERVER=xmpp.meet.jitsi" | runFileWrite -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : XMPP_SERVER"
result=$(echo "JVB_PORT=$usedport4" | runFileWrite -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_PORT"
result=$(echo "JVB_TCP_MAPPED_PORT=$usedport5" | runFileWrite -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_TCP_MAPPED_PORT"
result=$(echo "JVB_TCP_PORT=$usedport5" | runFileWrite -a "$containers_dir$app_name/.env")
checkSuccess "Updating .env file with missing option : JVB_TCP_PORT"
result=$(cd "$containers_dir$app_name" && runFileOp ./gen-passwords.sh)
checkSuccess "Running Jitsi Meet gen-passwords.sh script"
}
jitsimeet_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Adjusting $app_name docker system files for port changes."
echo ""
local result
result=$(runFileOp sed -i "s|80|$usedport1|g" $containers_dir$app_name/web/rootfs/defaults/default)
checkSuccess "Updating NGINX default site port 80 to $usedport1"
result=$(runFileOp sed -i "s|443|$usedport2|g" $containers_dir$app_name/web/rootfs/defaults/default)
checkSuccess "Updating NGINX default site port 443 to $usedport2"
dockerComposeRestart $app_name
}

View File

@ -9,7 +9,6 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"compression": "^1.8.1",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"express": "^4.17.1", "express": "^4.17.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -131,42 +130,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"dependencies": {
"mime-db": ">= 1.43.0 < 2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/compression/node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -684,14 +647,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",

View File

@ -4,7 +4,6 @@
"main": "server.js", "main": "server.js",
"dependencies": { "dependencies": {
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"compression": "^1.8.1",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"express": "^4.17.1", "express": "^4.17.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",

View File

@ -1,400 +0,0 @@
// Read-only Docker inspection routes for the Admin → System deep-dive pages.
//
// GET /api/system/containers
// List every container with a per-app summary (compose-project grouping
// mirrors what metrics_apps.json does, but with extra per-container
// fields the deep-dive pages need: state, status, image, ports, mounts,
// restart count). Cached for STAT_TTL_MS.
//
// GET /api/system/containers/:id
// Full container detail straight from `docker inspect`. Includes
// resource limits, mounts, networks, env, health-check state, restart
// policy. Cached briefly so the page can poll without thrashing the
// daemon.
//
// GET /api/system/containers/:id/stats
// One-shot live stats sample (one frame of /containers/<id>/stats).
// Returns the same shape Docker emits, plus a derived `cpu_percent`
// and `mem_percent` so the frontend doesn't have to recompute.
//
// GET /api/system/containers/:id/logs?tail=N
// Last N lines of combined stdout/stderr, multiplex-decoded.
//
// GET /api/system/storage
// `docker system df` — total + reclaimable for the engine overhead worth
// acting on (images, build cache). Cached for STORAGE_TTL_MS because this is
// one of the more expensive calls on a busy daemon. Named volumes and
// container writable layers are omitted: LibrePortal apps keep data in bind
// mounts, so both read ~empty here — per-app on-disk usage is generated
// separately (see webuiSystemAppStorage / /data/system/app_storage.json).
//
// Mounted at /api/system in routes.js (so paths are /api/system/containers
// etc.). Uses the shared docker util (utils/docker.js) which talks to the
// bind-mounted unix socket — no `docker` CLI inside the container, no
// extra deps.
const express = require('express');
const { dockerRequest, dockerStream, decodeMultiplexedLog, DOCKER_SOCKET } = require('../utils/docker.js');
const router = express.Router();
const STAT_TTL_MS = 1500;
const STORAGE_TTL_MS = 5000;
const LIST_TTL_MS = 1500;
// Trivial per-key TTL cache so concurrent tabs don't pile up daemon calls.
function makeTtlCache(ttl) {
const m = new Map();
return {
async get(key, loader) {
const now = Date.now();
const hit = m.get(key);
if (hit && (now - hit.at) < ttl) return hit.value;
const value = await loader();
m.set(key, { value, at: now });
return value;
},
invalidate(key) { m.delete(key); },
};
}
const listCache = makeTtlCache(LIST_TTL_MS);
const statCache = makeTtlCache(STAT_TTL_MS);
const inspectCache = makeTtlCache(STAT_TTL_MS);
const storageCache = makeTtlCache(STORAGE_TTL_MS);
// Container id slug used on the wire. Real Docker ids are 64-char hex; the
// short form is the first 12 chars. We accept either, plus container
// *names* (which can include /., _, -). Anything else is rejected.
const SAFE_CONTAINER_REF = /^[a-zA-Z0-9_.\-]{1,128}$/;
function safeRef(s) { return typeof s === 'string' && SAFE_CONTAINER_REF.test(s); }
function reduceContainer(c) {
// Compose project label tells us which "app" (in LibrePortal terms) a
// container belongs to. Containers without that label fall back to
// their own name as a single-container app.
const labels = c.Labels || {};
const project = labels['com.docker.compose.project'] || (c.Names && c.Names[0] ? c.Names[0].replace(/^\//, '') : null);
const service = labels['com.docker.compose.service'] || null;
const name = (c.Names && c.Names[0]) ? c.Names[0].replace(/^\//, '') : c.Id?.slice(0, 12) || '';
const networks = c.NetworkSettings && c.NetworkSettings.Networks
? Object.entries(c.NetworkSettings.Networks).map(([n, v]) => ({ name: n, ip: v?.IPAddress || null }))
: [];
const ports = Array.isArray(c.Ports)
? c.Ports.map(p => ({ ip: p.IP || null, host: p.PublicPort || null, container: p.PrivatePort, proto: p.Type }))
: [];
return {
id: c.Id,
short: (c.Id || '').slice(0, 12),
name,
image: c.Image,
image_id: c.ImageID,
project,
service,
state: c.State,
status: c.Status,
created: c.Created, // epoch seconds
labels,
ports,
networks,
mounts: Array.isArray(c.Mounts) ? c.Mounts.map(m => ({
type: m.Type,
source: m.Source,
target: m.Destination,
mode: m.Mode,
rw: m.RW,
})) : [],
};
}
// CPU% from a Docker stats frame. The daemon emits cumulative CPU usage in
// nanoseconds plus the system-wide CPU time; the percentage is the delta
// over the previous frame normalised by online CPUs. The first call has no
// prev frame, so Docker conveniently sends both `cpu_stats` (current) and
// `precpu_stats` (previous) in every frame.
function cpuPercent(s) {
const cpu = s.cpu_stats, pre = s.precpu_stats;
if (!cpu || !pre) return 0;
const cpuDelta = (cpu.cpu_usage?.total_usage || 0) - (pre.cpu_usage?.total_usage || 0);
const sysDelta = (cpu.system_cpu_usage || 0) - (pre.system_cpu_usage || 0);
const onlineCpus = cpu.online_cpus
|| (cpu.cpu_usage?.percpu_usage?.length)
|| 1;
if (sysDelta <= 0 || cpuDelta < 0) return 0;
return +((cpuDelta / sysDelta) * onlineCpus * 100).toFixed(2);
}
function memUsage(s) {
const m = s.memory_stats || {};
const usage = m.usage || 0;
// Docker counts page-cache in `usage`; the "real" working set excludes
// cached memory. Matches what `docker stats` shows.
const cache = (m.stats && (m.stats.cache || m.stats.total_inactive_file)) || 0;
const used = Math.max(0, usage - cache);
const limit = m.limit || 0;
return {
used, cache, limit,
percent: limit ? +((used / limit) * 100).toFixed(2) : 0,
};
}
function netUsage(s) {
const nets = s.networks || {};
let rx = 0, tx = 0;
for (const v of Object.values(nets)) {
rx += v.rx_bytes || 0;
tx += v.tx_bytes || 0;
}
return { rx_total: rx, tx_total: tx };
}
function blkioUsage(s) {
const b = (s.blkio_stats && s.blkio_stats.io_service_bytes_recursive) || [];
let read = 0, write = 0;
for (const e of b) {
if (e.op === 'Read' || e.op === 'read') read += e.value || 0;
else if (e.op === 'Write' || e.op === 'write') write += e.value || 0;
}
return { read, write };
}
function pidsUsage(s) {
const p = s.pids_stats || {};
return { current: p.current || 0, limit: p.limit || 0 };
}
// ---------------------------------------------------------------------------
router.get('/containers', async (req, res) => {
if (!DOCKER_SOCKET) return res.status(503).json({ error: 'docker_socket_unavailable' });
try {
const list = await listCache.get('all', () =>
dockerRequest('GET', '/containers/json', { all: 'true' })
);
const containers = (Array.isArray(list) ? list : []).map(reduceContainer);
// Group by project for convenience — frontend uses both shapes.
const byApp = new Map();
for (const c of containers) {
const key = c.project || c.name;
if (!byApp.has(key)) byApp.set(key, []);
byApp.get(key).push(c);
}
const apps = [...byApp.entries()].map(([app, members]) => {
const running = members.filter(c => c.state === 'running').length;
return { app, containers: members.length, running, members };
}).sort((a, b) => b.running - a.running || a.app.localeCompare(b.app));
res.set('Cache-Control', 'no-store');
res.json({ containers, apps, updated: new Date().toISOString() });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/containers/:id', async (req, res) => {
if (!DOCKER_SOCKET) return res.status(503).json({ error: 'docker_socket_unavailable' });
const { id } = req.params;
if (!safeRef(id)) return res.status(400).json({ error: 'invalid_id' });
try {
const detail = await inspectCache.get(`inspect:${id}`, () =>
dockerRequest('GET', `/containers/${encodeURIComponent(id)}/json`)
);
if (!detail) return res.status(404).json({ error: 'not_found' });
// Project the verbose inspect payload down to what the deep-dive
// page actually wants. Keeps the wire small and the frontend
// contract stable.
const host = detail.HostConfig || {};
const state = detail.State || {};
const cfg = detail.Config || {};
const netSettings = detail.NetworkSettings || {};
const out = {
id: detail.Id,
short: (detail.Id || '').slice(0, 12),
name: (detail.Name || '').replace(/^\//, ''),
image: cfg.Image || detail.Image,
image_id: detail.Image,
created: detail.Created,
project: (cfg.Labels && cfg.Labels['com.docker.compose.project']) || null,
service: (cfg.Labels && cfg.Labels['com.docker.compose.service']) || null,
labels: cfg.Labels || {},
state: {
status: state.Status,
running: !!state.Running,
paused: !!state.Paused,
restarting: !!state.Restarting,
oom_killed: !!state.OOMKilled,
dead: !!state.Dead,
pid: state.Pid || 0,
exit_code: state.ExitCode ?? null,
error: state.Error || '',
started_at: state.StartedAt || null,
finished_at: state.FinishedAt || null,
restart_count: detail.RestartCount || 0,
health: state.Health ? {
status: state.Health.Status,
failing_streak: state.Health.FailingStreak,
log: (state.Health.Log || []).slice(-5).map(l => ({
start: l.Start, end: l.End, exit_code: l.ExitCode, output: (l.Output || '').slice(0, 800)
})),
} : null,
},
limits: {
memory: host.Memory || 0, // bytes; 0 = unlimited
memory_swap: host.MemorySwap || 0,
memory_reservation: host.MemoryReservation || 0,
cpu_shares: host.CpuShares || 0,
cpu_quota: host.CpuQuota || 0, // microseconds in CpuPeriod
cpu_period: host.CpuPeriod || 0,
nano_cpus: host.NanoCpus || 0, // 1e9 = 1 cpu
pids: host.PidsLimit || 0,
restart_policy: (host.RestartPolicy && host.RestartPolicy.Name) || 'no',
restart_max: (host.RestartPolicy && host.RestartPolicy.MaximumRetryCount) || 0,
},
mounts: (detail.Mounts || []).map(m => ({
type: m.Type,
source: m.Source,
target: m.Destination,
mode: m.Mode,
rw: m.RW,
})),
networks: netSettings.Networks
? Object.entries(netSettings.Networks).map(([name, v]) => ({
name,
ip: v?.IPAddress || null,
mac: v?.MacAddress || null,
gateway: v?.Gateway || null,
}))
: [],
ports: netSettings.Ports
? Object.entries(netSettings.Ports).flatMap(([k, bindings]) => {
const [containerPort, proto] = k.split('/');
if (!Array.isArray(bindings) || !bindings.length) {
return [{ container: parseInt(containerPort, 10), proto, host: null, ip: null }];
}
return bindings.map(b => ({
container: parseInt(containerPort, 10),
proto,
host: b.HostPort ? parseInt(b.HostPort, 10) : null,
ip: b.HostIp || null,
}));
})
: [],
};
res.set('Cache-Control', 'no-store');
res.json(out);
} catch (err) {
if (/404/.test(err.message)) return res.status(404).json({ error: 'not_found' });
res.status(500).json({ error: err.message });
}
});
router.get('/containers/:id/stats', async (req, res) => {
if (!DOCKER_SOCKET) return res.status(503).json({ error: 'docker_socket_unavailable' });
const { id } = req.params;
if (!safeRef(id)) return res.status(400).json({ error: 'invalid_id' });
try {
const sample = await statCache.get(`stats:${id}`, () =>
dockerRequest('GET', `/containers/${encodeURIComponent(id)}/stats`, { stream: 'false' })
);
if (!sample) return res.status(404).json({ error: 'not_found' });
res.set('Cache-Control', 'no-store');
res.json({
t: Date.now(),
cpu_percent: cpuPercent(sample),
memory: memUsage(sample),
network: netUsage(sample),
blkio: blkioUsage(sample),
pids: pidsUsage(sample),
});
} catch (err) {
if (/404/.test(err.message)) return res.status(404).json({ error: 'not_found' });
res.status(500).json({ error: err.message });
}
});
router.get('/containers/:id/logs', async (req, res) => {
if (!DOCKER_SOCKET) return res.status(503).json({ error: 'docker_socket_unavailable' });
const { id } = req.params;
if (!safeRef(id)) return res.status(400).json({ error: 'invalid_id' });
const tail = Math.max(1, Math.min(2000, parseInt(req.query.tail, 10) || 200));
try {
const { stream } = await dockerStream(`/containers/${encodeURIComponent(id)}/logs`, {
stdout: 'true', stderr: 'true', tail: String(tail), timestamps: 'true',
});
const chunks = [];
for await (const c of stream) chunks.push(c);
const text = decodeMultiplexedLog(Buffer.concat(chunks));
res.set('Cache-Control', 'no-store');
res.type('text/plain').send(text);
} catch (err) {
if (/404/.test(err.message)) return res.status(404).json({ error: 'not_found' });
res.status(500).json({ error: err.message });
}
});
router.get('/storage', async (req, res) => {
if (!DOCKER_SOCKET) return res.status(503).json({ error: 'docker_socket_unavailable' });
try {
const df = await storageCache.get('df', () => dockerRequest('GET', '/system/df'));
if (!df) return res.status(500).json({ error: 'no_data' });
// Roll the verbose response up into headline numbers per category. We
// surface only the engine overhead worth acting on — images and build
// cache — and skip container writable layers: for LibrePortal that's a
// near-zero scratch number (app data lives in bind mounts, shown per-app
// elsewhere) that just confuses the picture.
// "Reclaimable" reflects exactly what the Reclaim button frees: dangling
// images + the whole build cache. Tagged-but-unused images are
// deliberately NOT counted — the safe prune leaves them alone — so the
// headline matches the button's effect instead of overstating it.
const isDangling = (im) => {
const tags = im.RepoTags || [];
return tags.length === 0 || tags.every(t => t.includes('<none>'));
};
const sumImages = (df.Images || []).reduce(
(a, im) => {
a.count++;
a.size += im.Size || 0;
a.shared += im.SharedSize || 0;
if (isDangling(im)) a.reclaimable += im.Size || 0;
return a;
},
{ count: 0, size: 0, shared: 0, reclaimable: 0 }
);
const sumBuild = (df.BuildCache || []).reduce(
(a, b) => {
a.count++;
a.size += b.Size || 0;
if (!b.InUse) a.reclaimable += b.Size || 0;
return a;
},
{ count: 0, size: 0, reclaimable: 0 }
);
// Every image, largest first — the Storage page lists them all so the
// user can remove specific ones (deletion runs through the task/CLI,
// not from here). Dangling <none> images are included on purpose.
const images = (df.Images || [])
.slice()
.sort((a, b) => (b.Size || 0) - (a.Size || 0))
.map(im => ({
id: im.Id,
repo_tags: im.RepoTags || [],
size: im.Size || 0,
shared_size: im.SharedSize || 0,
containers: im.Containers || 0,
created: im.Created,
}));
const total = sumImages.size + sumBuild.size;
const reclaimable = sumImages.reclaimable + sumBuild.reclaimable;
res.set('Cache-Control', 'no-store');
res.json({
total, reclaimable,
images: sumImages,
build_cache: sumBuild,
image_list: images,
updated: new Date().toISOString(),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;

View File

@ -1,58 +0,0 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const FEATURES_DIR = path.join(__dirname, '..', '..', 'frontend', 'components');
/* =========================
GET /api/features/list
Walks frontend/components/<id>/ and returns one entry per directory that
contains a feature.json the WebUI's page manifest, discovered from the
folders themselves (exactly how /api/themes/list discovers themes). Drop a
components/<id>/ folder in and its page appears; delete it and the page is
gone no central edit. The navigation kernel fetches this and falls back to
the checked-in components/manifest.dev.json if the API is unavailable.
Each feature.json declares: id, routes[], optional module (self-registering
index.js), optional handler (legacy fallback method), navId, nav{}, and order
(controls list + route-precedence ordering e.g. apps before app-detail so
the '/apps*' wildcard wins over '/app*').
Public the page list isn't sensitive and the kernel needs it before login
to render the right route (same rationale as the themes list).
========================= */
router.get('/list', (req, res) => {
const features = [];
try {
if (fs.existsSync(FEATURES_DIR)) {
for (const entry of fs.readdirSync(FEATURES_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const metaPath = path.join(FEATURES_DIR, entry.name, 'feature.json');
if (!fs.existsSync(metaPath)) continue;
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
if (meta && meta.id && Array.isArray(meta.routes)) {
features.push(meta);
} else {
console.warn(`[features] ${entry.name}/feature.json missing id/routes — skipped`);
}
} catch (e) {
console.warn(`[features] ${entry.name}/feature.json is malformed — skipped:`, e.message);
}
}
}
} catch (err) {
console.error('Error scanning features directory:', err);
}
// Ascending `order` controls both nav order and route-registration order
// (the latter preserves wildcard precedence). Missing order sorts last.
features.sort((a, b) => ((a.order ?? 999) - (b.order ?? 999)));
res.json({ version: 1, source: 'scan', features });
});
module.exports = router;

View File

@ -11,13 +11,10 @@ const PATHS = {
const themeRoutes = require('./theme.js'); const themeRoutes = require('./theme.js');
const themesRoutes = require('./themes.js'); const themesRoutes = require('./themes.js');
const featuresRoutes = require('./features.js');
const authRoutes = require('./auth-routes.js'); const authRoutes = require('./auth-routes.js');
const taskRoutes = require('./task-routes.js'); const taskRoutes = require('./task-routes.js');
const serviceRoutes = require('./service-routes.js'); const serviceRoutes = require('./service-routes.js');
const setupRoutes = require('./setup-routes.js'); const setupRoutes = require('./setup-routes.js');
const systemRoutes = require('./system-routes.js');
const dockerInfoRoutes = require('./docker-info-routes.js');
const { testConnection } = require('../utils/mail.js'); const { testConnection } = require('../utils/mail.js');
module.exports = { module.exports = {
@ -27,17 +24,12 @@ module.exports = {
// Theme discovery is public so the login overlay can pick the right // Theme discovery is public so the login overlay can pick the right
// palette before the user logs in. // palette before the user logs in.
app.use('/api/themes', themesRoutes); app.use('/api/themes', themesRoutes);
// Feature/page discovery is public for the same reason — the navigation
// kernel needs the page manifest before login to route the first paint.
app.use('/api/features', featuresRoutes);
// Protected API routes // Protected API routes
app.use('/api/theme', requireAuth, themeRoutes); app.use('/api/theme', requireAuth, themeRoutes);
app.use('/api/tasks', taskRoutes); // requireAuth applied per-route inside app.use('/api/tasks', taskRoutes); // requireAuth applied per-route inside
app.use('/api/apps', serviceRoutes); // requireAuth applied per-route inside app.use('/api/apps', serviceRoutes); // requireAuth applied per-route inside
app.use('/api/setup', setupRoutes); // requireAuth applied per-route inside app.use('/api/setup', setupRoutes); // requireAuth applied per-route inside
app.use('/api/system', requireAuth, systemRoutes); // live host metrics (/proc)
app.use('/api/system', requireAuth, dockerInfoRoutes); // /containers/*, /storage
app.post('/api/test-mail-connection', requireAuth, testConnection); app.post('/api/test-mail-connection', requireAuth, testConnection);
app.post('/api/gluetun/mullvad-wireguard', requireAuth, async (req, res) => { app.post('/api/gluetun/mullvad-wireguard', requireAuth, async (req, res) => {

View File

@ -11,11 +11,9 @@
// directly over the unix socket. That means no extra system deps and // directly over the unix socket. That means no extra system deps and
// no group-level privilege grants — node only sees what the mounted // no group-level privilege grants — node only sees what the mounted
// socket lets it see. // socket lets it see.
// - This file is READ-ONLY surface (status + log tails). Restarting a // - Restart still goes through the existing task system. The bash task
// service is a mutation, so it goes through the task system like every // processor runs on the host (where `docker` IS available) so its
// other mutation: the Services tab dispatches a `service_restart` task // `docker compose restart …` command works fine.
// (core/tasks) that runs `libreportal app restart <app> <service>` on
// the host, where `docker` IS available and runs as the right user.
// - URLs / port chips for each service are read client-side from the // - URLs / port chips for each service are read client-side from the
// existing /data/apps/generated/apps-services.json — no backend // existing /data/apps/generated/apps-services.json — no backend
// surface needed for that. // surface needed for that.
@ -27,10 +25,14 @@ const path = require('path');
const http = require('http'); const http = require('http');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { requireAuth } = require('../utils/middleware.js'); const { requireAuth } = require('../utils/middleware.js');
const { pokeFifo } = require('../utils/fifo.js');
const { fileConfig } = require('../utils/config.js'); const { fileConfig } = require('../utils/config.js');
const router = express.Router(); const router = express.Router();
const TASKS_DIR = path.join(__dirname, '..', '..', 'frontend', 'data', 'tasks');
const FIFO_PATH = path.join(TASKS_DIR, '.queue.fifo');
const CONTAINERS_DIR = '/docker/containers';
const APPS_SERVICES_JSON = path.join(__dirname, '..', '..', 'frontend', 'data', 'apps', 'generated', 'apps-services.json'); const APPS_SERVICES_JSON = path.join(__dirname, '..', '..', 'frontend', 'data', 'apps', 'generated', 'apps-services.json');
// ===================================================================== // =====================================================================
@ -303,6 +305,10 @@ async function lookupServiceTransport(appName, serviceName) {
return { transport: 'docker' }; return { transport: 'docker' };
} }
function appComposeFile(appName) {
return path.join(CONTAINERS_DIR, appName, 'docker-compose.yml');
}
// ===================================================================== // =====================================================================
// GET /api/apps/:appName/services/status // GET /api/apps/:appName/services/status
// → [{ serviceName, state, statusText, containerName, containerId }] // → [{ serviceName, state, statusText, containerName, containerId }]
@ -362,6 +368,50 @@ router.get('/:appName/services/status', requireAuth, async (req, res) => {
} }
}); });
// =====================================================================
// POST /api/apps/:appName/services/:serviceName/restart
// Creates a task that runs `docker compose restart <service>` on the
// host. The host has `docker` available; this container does not.
// =====================================================================
router.post('/:appName/services/:serviceName/restart', requireAuth, async (req, res) => {
const { appName, serviceName } = req.params;
if (!safeName(appName) || !safeName(serviceName)) {
return res.status(400).json({ error: 'Invalid app or service name' });
}
const compose = appComposeFile(appName);
if (!fs.existsSync(compose)) {
return res.status(404).json({ error: `Compose file not found: ${compose}` });
}
const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const task = {
id,
command: `docker compose -f "${compose}" restart "${serviceName}"`,
type: 'service-restart',
app: appName,
config: serviceName,
status: 'queued',
createdAt: new Date().toISOString(),
startedAt: null,
completedAt: null,
heartbeatAt: null,
exitCode: null,
errorMessage: null
};
try {
await fsp.mkdir(TASKS_DIR, { recursive: true });
const taskPath = path.join(TASKS_DIR, `${id}.json`);
const tmp = `${taskPath}.tmp`;
await fsp.writeFile(tmp, JSON.stringify(task, null, 2));
await fsp.rename(tmp, taskPath);
pokeFifo(FIFO_PATH, id);
res.status(201).json(task);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ===================================================================== // =====================================================================
// GET /api/apps/:appName/services/:serviceName/logs // GET /api/apps/:appName/services/:serviceName/logs
// SSE-wraps the Docker /containers/<id>/logs?follow=1 stream. // SSE-wraps the Docker /containers/<id>/logs?follow=1 stream.

View File

@ -154,15 +154,6 @@ router.post('/save', requireAuth, async (req, res) => {
return res.status(400).json({ error: 'timezone required' }); return res.status(400).json({ error: 'timezone required' });
} }
// Experience level seeds the WebUI's Beginner/Advanced UI mode default.
// Optional — old WebUIs may not send it — and constrained to the
// enum so a bad value can't smuggle anything into the bash applier.
if (payload.install_level !== undefined) {
if (payload.install_level !== 'beginner' && payload.install_level !== 'advanced') {
return res.status(400).json({ error: 'invalid install_level' });
}
}
// Domains are optional but each entry must be a valid hostname. Cap at // Domains are optional but each entry must be a valid hostname. Cap at
// 9 because the config schema only has CFG_DOMAIN_1..CFG_DOMAIN_9. // 9 because the config schema only has CFG_DOMAIN_1..CFG_DOMAIN_9.
const domainRe = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i; const domainRe = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;

View File

@ -1,299 +0,0 @@
// Live system metrics — the fast path behind the Admin → System gauges and the
// dashboard "pulse" tiles.
//
// Periodic host-side data (disks, network, docker, per-app, 24 h history) is
// produced by the webui_system_metrics generator into frontend/data/system/.
// This file serves the *live* path: CPU / memory / load read straight from
// /proc, optionally fused with the latest host JSON snapshot so a single SSE
// message carries everything a client needs to draw a frame.
//
// Endpoints:
// GET /live — single-shot JSON snapshot (kept for callers that still poll)
// GET /stream — Server-Sent Events; pushes a fused sample once per second.
// One /proc read per second across all subscribers (shared
// ticker), so 100 open tabs still cost one read/sec.
//
// Namespace note: this runs *inside* the libreportal container. /proc/stat,
// /proc/meminfo and /proc/loadavg are not namespaced, so they report host-wide
// values that match the generator's numbers. /proc/net/dev IS per-netns (it
// would show only this container's traffic), so the host generator owns
// network/disk and we splice its latest snapshot into each SSE message.
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const metricsWriter = require('../utils/metrics-writer.js');
const router = express.Router();
const CORES = os.cpus().length || 1;
const MIN_INTERVAL_MS = 750; // serve cache to anything faster than this
const STREAM_TICK_MS = 1000; // SSE push cadence — 1 Hz live feel
const HEARTBEAT_MS = 25000; // SSE comment frame to keep proxies from idling out
const HOST_JSON_DIR = path.join(__dirname, '..', '..', 'frontend', 'data', 'system');
let prevCpu = null; // { total, idle } from the last read
let cache = null; // { sample, at }
let inflight = null; // dedupe concurrent cache-miss reads
async function readCpu() {
const data = await fs.readFile('/proc/stat', 'utf8');
const first = data.split('\n', 1)[0]; // "cpu u n s i io irq sirq steal ..."
const n = first.trim().split(/\s+/).slice(1).map(Number);
const idle = (n[3] || 0) + (n[4] || 0); // idle + iowait
const total = n.reduce((a, b) => a + (b || 0), 0);
return { total, idle };
}
async function readMem() {
const data = await fs.readFile('/proc/meminfo', 'utf8');
const m = {};
for (const line of data.split('\n')) {
const mm = line.match(/^(\w+):\s+(\d+)/);
if (mm) m[mm[1]] = parseInt(mm[2], 10) * 1024; // kB -> bytes
}
const total = m.MemTotal || 0;
const available = m.MemAvailable || 0;
const used = Math.max(0, total - available);
const swapTotal = m.SwapTotal || 0;
const swapUsed = Math.max(0, swapTotal - (m.SwapFree || 0));
return {
total, used, available,
percent: total ? +(used / total * 100).toFixed(1) : 0,
swap_total: swapTotal, swap_used: swapUsed,
swap_percent: swapTotal ? +(swapUsed / swapTotal * 100).toFixed(1) : 0
};
}
async function readLoad() {
const data = await fs.readFile('/proc/loadavg', 'utf8');
const [l1, l5, l15] = data.trim().split(/\s+/).map(Number);
return { load1: l1 || 0, load5: l5 || 0, load15: l15 || 0 };
}
async function sample() {
const [cpuNow, memory, load] = await Promise.all([readCpu(), readMem(), readLoad()]);
let percent = 0;
if (prevCpu) {
const dt = cpuNow.total - prevCpu.total;
const di = cpuNow.idle - prevCpu.idle;
if (dt > 0) percent = +Math.max(0, Math.min(100, (1 - di / dt) * 100)).toFixed(1);
}
prevCpu = cpuNow;
return {
cpu: {
percent,
cores: CORES,
load1: load.load1, load5: load.load5, load15: load.load15,
load1_percent: +Math.min(100, load.load1 / CORES * 100).toFixed(1)
},
memory,
t: Date.now()
};
}
router.get('/live', async (req, res) => {
const now = Date.now();
if (cache && (now - cache.at) < MIN_INTERVAL_MS) {
res.set('Cache-Control', 'no-store');
return res.json(cache.sample);
}
try {
if (!inflight) {
inflight = sample()
.then((s) => { cache = { sample: s, at: Date.now() }; return s; })
.finally(() => { inflight = null; });
}
const s = await inflight;
res.set('Cache-Control', 'no-store');
res.json(s);
} catch (err) {
res.status(500).json({ error: 'metrics_unavailable' });
}
});
// ---------------------------------------------------------------------------
// SSE live stream
// ---------------------------------------------------------------------------
// One ticker for the whole process. Subscribers join/leave; the ticker only
// runs while at least one is connected, so an idle WebUI costs nothing.
const subscribers = new Set();
let tickHandle = null;
let heartbeatHandle = null;
let lastSample = null;
let hostJson = { metrics: null, disk: null, memory: null, apps: null };
let hostJsonLoadedAt = 0;
const HOST_JSON_REFRESH_MS = 5000; // re-read host snapshots every 5 s (they regen at most 1×/min)
// Read a JSON file but never throw — missing/invalid → previous value.
async function readJsonSafe(file, fallback = null) {
try {
const txt = await fs.readFile(file, 'utf8');
return JSON.parse(txt);
} catch (_) {
return fallback;
}
}
// Refresh the cached host-side JSON if it's been at least HOST_JSON_REFRESH_MS
// since the last read. Cheap when the files haven't changed because the OS
// page cache makes the read essentially free.
async function refreshHostJson(now) {
if (now - hostJsonLoadedAt < HOST_JSON_REFRESH_MS) return;
hostJsonLoadedAt = now;
const [metrics, disk, memory, apps] = await Promise.all([
readJsonSafe(path.join(HOST_JSON_DIR, 'metrics.json'), hostJson.metrics),
readJsonSafe(path.join(HOST_JSON_DIR, 'disk_usage.json'), hostJson.disk),
readJsonSafe(path.join(HOST_JSON_DIR, 'memory_usage.json'), hostJson.memory),
readJsonSafe(path.join(HOST_JSON_DIR, 'metrics_apps.json'), hostJson.apps)
]);
hostJson = { metrics, disk, memory, apps };
}
function ssePayload(s) {
// Fuse the live in-container sample with the latest host-side snapshot so
// a client gets everything it needs from one stream. The host fields tick
// slowly (≤ 1/min) but live alongside the 1 Hz CPU/mem feed.
const m = hostJson.metrics || {};
return {
t: s.t,
cpu: s.cpu,
memory: s.memory,
disks: Array.isArray(m.disks) ? m.disks : [],
network: m.network || { rx_rate: 0, tx_rate: 0 },
docker: m.docker || null,
apps: (hostJson.apps && Array.isArray(hostJson.apps.apps)) ? hostJson.apps.apps : []
};
}
async function tick() {
if (subscribers.size === 0) { // nothing to do — defensive
stopTicker();
return;
}
try {
const s = await sample();
const now = Date.now();
cache = { sample: s, at: now };
await refreshHostJson(now);
const payload = ssePayload(s);
lastSample = payload;
const frame = `data: ${JSON.stringify(payload)}\n\n`;
for (const res of subscribers) {
try { res.write(frame); } catch (_) { /* will be reaped on close */ }
}
} catch (_) { /* swallow — try again next tick */ }
}
function startTicker() {
if (tickHandle) return;
tick(); // fire immediately so the first frame is fresh
tickHandle = setInterval(tick, STREAM_TICK_MS);
// Heartbeat keeps proxies (Traefik/nginx) from idling the connection out;
// SSE comments start with ":" and are ignored by EventSource.
heartbeatHandle = setInterval(() => {
for (const res of subscribers) {
try { res.write(': hb\n\n'); } catch (_) {}
}
}, HEARTBEAT_MS);
}
function stopTicker() {
if (tickHandle) { clearInterval(tickHandle); tickHandle = null; }
if (heartbeatHandle) { clearInterval(heartbeatHandle); heartbeatHandle = null; }
}
router.get('/stream', async (req, res) => {
// SSE handshake. `no-transform` tells the compression middleware not to
// gzip this response (gzip buffers and would break streaming). `X-Accel-
// Buffering: no` tells nginx/Traefik to flush each event immediately.
res.set({
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-store, no-transform',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no'
});
res.flushHeaders?.();
// Initial "retry" hint — if the connection dies the browser will reopen
// after this many ms (default 3000 is fine but explicit is clearer).
res.write('retry: 3000\n\n');
subscribers.add(res);
startTicker();
// If we already have a fresh sample, ship it right now so the client doesn't
// have to wait STREAM_TICK_MS for its first frame.
if (lastSample) {
try { res.write(`data: ${JSON.stringify(lastSample)}\n\n`); } catch (_) {}
}
const cleanup = () => {
subscribers.delete(res);
if (subscribers.size === 0) stopTicker();
};
req.on('close', cleanup);
req.on('error', cleanup);
});
// ---------------------------------------------------------------------------
// History range query
// ---------------------------------------------------------------------------
// `range` is minutes back from now (1..10080 = 7 days). `keys` is an optional
// comma-list of metric names to project (defaults to the whole point).
//
// Two on-disk binary rings back this:
// 1m tier — 1440 pts @ 1-min (24 h)
// 5m tier — 2016 pts @ 5-min ( 7 d)
//
// Tier auto-selects from `range`: ≤ 1440 reads the 1m ring point-for-point;
// > 1440 reads the 5m ring (range/5 points). Caller can override with
// `?tier=1m|5m`. Falls back to the legacy JSON only if the binary ring is
// completely empty (e.g. fresh container, writer hasn't filled it yet).
const HISTORY_MAX_MIN = 10080; // 7 days
router.get('/history', async (req, res) => {
const range = Math.max(1, Math.min(HISTORY_MAX_MIN, parseInt(req.query.range, 10) || 60));
const tier = req.query.tier === '5m' || req.query.tier === '1m'
? req.query.tier
: (range > 1440 ? '5m' : '1m');
const wantPoints = tier === '5m' ? Math.ceil(range / 5) : range;
const keys = typeof req.query.keys === 'string' && req.query.keys.length
? req.query.keys.split(',').map((s) => s.trim()).filter(Boolean)
: null;
try {
let pts = await metricsWriter.read(wantPoints, tier);
let updated = null;
if (pts.length) {
updated = new Date(pts[pts.length - 1].t * 1000).toISOString();
} else {
// Cold start: writer hasn't filled the ring yet. Serve from the
// legacy JSON so the UI still has something to draw.
const file = path.join(HOST_JSON_DIR, 'metrics_history.json');
try {
const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
pts = (Array.isArray(parsed?.points) ? parsed.points : []).slice(-wantPoints);
updated = parsed?.updated || null;
} catch (_) { /* leave pts empty */ }
}
const points = keys
? pts.map((p) => {
const out = { t: p.t };
for (const k of keys) if (k in p) out[k] = p[k];
return out;
})
: pts;
res.set('Cache-Control', 'no-store');
res.json({ range, tier, points, updated });
} catch (_) {
res.status(500).json({ error: 'history_unavailable', points: [] });
}
});
// Kick the persistent 1-min writer. It needs the same `sample()` we use for
// the SSE stream — passed in to avoid a circular require.
metricsWriter.start({
sampleFn: sample,
hostJsonFn: () => hostJson,
});
module.exports = router;

View File

@ -42,7 +42,7 @@ const webuiLoginsConfig = parseConfigFile(path.join(__dirname, '..', '..', 'webu
const webuiLogsConfig = parseConfigFile(path.join(__dirname, '..', '..', 'webui_logs')); const webuiLogsConfig = parseConfigFile(path.join(__dirname, '..', '..', 'webui_logs'));
// Merge: later sources override earlier. webui_logins / webui_logs hold // Merge: later sources override earlier. webui_logins / webui_logs hold
// CFG_WEBUI_* keys generated from the system configs/webui/* and bind-mounted // CFG_WEBUI_* keys generated from /docker/configs/webui/* and bind-mounted
// in via libreportal's compose. // in via libreportal's compose.
const fileConfig = { ...libreportalConfig, ...webuiLoginsConfig, ...webuiLogsConfig }; const fileConfig = { ...libreportalConfig, ...webuiLoginsConfig, ...webuiLogsConfig };

Some files were not shown because too many files have changed in this diff Show More