diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-07-09 21:49:56 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-07-09 21:49:56 -0700 |
| commit | 1d043702ef136e8f2e516c5304a57be5718f1f6f (patch) | |
| tree | 9b3ad070f11dad5994c58acd41a15db8043745aa | |
12 services: adguard, ampache, azuracast, flood, glance, glances,
grafana, immich, invidious, jellyfin, openwebui, watchtower
Each service includes a docker-compose.yml, README.md with setup
instructions, and .env.example where applicable.
51 files changed, 2799 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..baf8438 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Environment files with secrets +.env +!.env.example + +# Stack-specific data/config directories +adguard/config/ +adguard/data/ +ampache/data/ +azuracast/azuracast.env_backup_* +azuracast/.env_backup_* +azuracast/docker-compose.backup.yml +azuracast/.claude/ +flood/flood-config/ +glance/assets/ +glance/config/ +immich/config/ +immich/postgres/ +jellyfin/cache/ +jellyfin/config/ +jellyfin/jackett-config/ +jellyfin/prowlarr-config/ +jellyfin/radarr-config/ +jellyfin/recyclarr-config/ +jellyfin/sonarr-config/ +invidious/assets/ +invidious/config/config.example.yml +invidious/config/migrate-scripts/ +invidious/.git/ +invidious/screenshots/ +invidious/locales/ +invidious/mocks/ +invidious/spec/ +invidious/src/ +invidious/scripts/ +invidious/CHANGELOG*.md +invidious/.ameba.yml +invidious/.editorconfig +invidious/.gitattributes +invidious/.github/ +invidious/.gitignore +invidious/.gitmodules +invidious/LICENSE +invidious/Makefile +invidious/shard.lock +invidious/shard.yml +invidious/TRANSLATION +invidious/videojs-dependencies.yml +invidious/kubernetes/ +invidious/invidious.service + +# System files +.DS_Store +*.swp +*.swo diff --git a/README.md b/README.md new file mode 100644 index 0000000..828af0f --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Docker Stack + +Docker Compose files for my self-hosted services. + +## Services + +| Service | Description | Directory | +|---------|------------|-----------| +| AdGuard Home | DNS ad-blocker | [`adguard/`](adguard/) | +| Ampache | Music streaming server | [`ampache/`](ampache/) | +| AzuraCast | Web radio management | [`azuracast/`](azuracast/) | +| Flood | Web UI for rTorrent | [`flood/`](flood/) | +| Glance | Personal dashboard | [`glance/`](glance/) | +| Glances | System monitoring | [`glances/`](glances/) | +| Grafana + Prometheus | Metrics & monitoring | [`grafana/`](grafana/) | +| Immich | Photo/video management | [`immich/`](immich/) | +| Invidious | YouTube frontend | [`invidious/`](invidious/) | +| Jellyfin + *arr Stack | Media server suite | [`jellyfin/`](jellyfin/) | +| Open WebUI | LLM chat interface | [`openwebui/`](openwebui/) | +| Watchtower | Automatic container updates | [`watchtower/`](watchtower/) | + +## Usage + +Each service directory contains: +- `docker-compose.yml` — The compose file +- `README.md` — Setup instructions and configuration guide +- `.env.example` — Template for required environment variables (where applicable) + +To deploy a service, cd into its directory and follow its README. + +### Quick Start (any service) + +```bash +cd <service> +cp .env.example .env # if present — edit with your settings +docker compose up -d +``` + +## Notes + +- All `.env` files are git-ignored. Each service provides a `.env.example` as a template. +- Secrets and API keys in the compose files reference environment variables — provide them via `.env` files. +- Volume paths and timezone settings are configured for my environment. Adjust them in the compose files or `.env` for your setup. diff --git a/adguard/README.md b/adguard/README.md new file mode 100644 index 0000000..40c7672 --- /dev/null +++ b/adguard/README.md @@ -0,0 +1,24 @@ +# AdGuard Home + +DNS ad-blocker and network-level tracker blocker. + +## Setup + +1. Create required directories: + ``` + mkdir -p config data + ``` + +2. Start: + ``` + docker compose up -d + ``` + +## Access + +- Web UI: `http://<host>:4000` + +## What to Configure + +- **Ports** — Port 53 (DNS) must be available. If something else on your system is already listening on port 53 (e.g. systemd-resolved), disable it or change the host ports. +- **Subnet** — The network subnet (`172.30.0.0/24`) can be adjusted if it conflicts with your LAN. diff --git a/adguard/docker-compose.yml b/adguard/docker-compose.yml new file mode 100644 index 0000000..053605a --- /dev/null +++ b/adguard/docker-compose.yml @@ -0,0 +1,29 @@ +services: + adguardhome: + image: adguard/adguardhome + container_name: adguardhome + restart: unless-stopped + volumes: + - ./data:/opt/adguardhome/work + - ./config:/opt/adguardhome/conf + ports: + - "53:53/tcp" + - "53:53/udp" + - "67:67/udp" + - "68:68/udp" + - "4000:4000/tcp" + - "853:853/tcp" # DNS-over-TLS + - "853:853/udp" # DNS-over-QUIC + - "5443:5443/tcp" # DNS-over-HTTPS (alt) + - "5443:5443/udp" + - "6060:6060/tcp" # pprof debug + networks: + - adguard + +networks: + adguard: + driver: bridge + ipam: + config: + - subnet: 172.30.0.0/24 + gateway: 172.30.0.1 diff --git a/ampache/.env.example b/ampache/.env.example new file mode 100644 index 0000000..375ffc7 --- /dev/null +++ b/ampache/.env.example @@ -0,0 +1,15 @@ +# Amplitude configuration +MYSQL_USER=admin +MYSQL_PASS=your_mysql_password_here +# DISABLE_INOTIFYWAIT_CLEAN=0 +# DB_NAME= +# DB_USER= +# DB_PASSWORD= +# DB_HOST= +# DB_PORT=3306 +# FORCE_INSTALL=0 +# AMPACHE_DB_USER= +# AMPACHE_DB_PASSWORD= +# AMPACHE_ADMIN_USER=admin +# AMPACHE_ADMIN_PASSWORD=**Random** +# AMPACHE_ADMIN_EMAIL= diff --git a/ampache/README.md b/ampache/README.md new file mode 100644 index 0000000..503eefc --- /dev/null +++ b/ampache/README.md @@ -0,0 +1,24 @@ +# Ampache + +Self-hosted music streaming server. + +## Setup + +1. Copy `.env.example` to `.env` and fill in your credentials: + ``` + cp .env.example .env + ``` + +2. Start: + ``` + docker compose up -d + ``` + +## Access + +- Web UI: `http://<host>:7676` + +## What to Configure + +- **`.env`** — Set `MYSQL_USER` and `MYSQL_PASS` before first start. Other variables can be set after the initial setup. +- **Media volume** — The line `- /srv/mus:/media` mounts your music library. Change `/srv/mus` to wherever your music is stored. diff --git a/ampache/docker-compose.yml b/ampache/docker-compose.yml new file mode 100644 index 0000000..b7d398f --- /dev/null +++ b/ampache/docker-compose.yml @@ -0,0 +1,29 @@ +name: ampache + +services: + ampache: + image: ampache/ampache:latest + container_name: ampache + restart: unless-stopped + ports: + - 7676:80 + volumes: + - ./data/config:/var/www/config + - ./data/log:/var/log/ampache + - /srv/mus:/media + - ./data/mysql:/var/lib/mysql + environment: + MYSQL_USER: ${MYSQL_USER-admin} + MYSQL_PASS: ${MYSQL_PASS-**Random**} + DB_NAME: ${DB_NAME-} + DB_HOST: ${DB_HOST-} + DB_USER: ${DB_USER-} + DB_PASSWORD: ${DB_PASSWORD-} + DB_PORT: ${DB_PORT-} + DISABLE_INOTIFYWAIT_CLEAN: ${DISABLE_INOTIFYWAIT_CLEAN-0} + FORCE_INSTALL: ${FORCE_INSTALL-0} + AMPACHE_DB_USER: ${AMPACHE_DB_USER-} + AMPACHE_DB_PASSWORD: ${AMPACHE_DB_PASSWORD-} + AMPACHE_ADMIN_USER: ${AMPACHE_ADMIN_USER-} + AMPACHE_ADMIN_PASSWORD: ${AMPACHE_ADMIN_PASSWORD-} + AMPACHE_ADMIN_EMAIL: ${AMPACHE_ADMIN_EMAIL-} diff --git a/azuracast/.env.example b/azuracast/.env.example new file mode 100644 index 0000000..de840e3 --- /dev/null +++ b/azuracast/.env.example @@ -0,0 +1,48 @@ +# This file was automatically generated by AzuraCast. +# You can modify it as necessary. To apply changes, restart the Docker containers. +# Remove the leading "#" symbol from lines to uncomment them. + +# (Docker Compose) All Docker containers are prefixed by this name. Do not change this after installation. +# Default: azuracast +COMPOSE_PROJECT_NAME=azuracast + +# (Docker Compose) The amount of time to wait before a Docker Compose operation fails. Increase this on lower performance computers. +# Default: 300 +COMPOSE_HTTP_TIMEOUT=300 + +# Release Channel +# Valid options: latest, stable +# Default: latest +AZURACAST_VERSION=latest + +# HTTP Port +# The main port AzuraCast listens to for insecure HTTP connections. +# Default: 80 +AZURACAST_HTTP_PORT=8080 + +# HTTPS Port +# The main port AzuraCast listens to for secure HTTPS connections. +# Default: 443 +AZURACAST_HTTPS_PORT=8443 + +# SFTP Port +# The port AzuraCast listens to for SFTP file management connections. +# Default: 2022 +# AZURACAST_SFTP_PORT=2022 + +# Station Ports +# The ports AzuraCast should listen to for station broadcasts and incoming DJ connections. +# AZURACAST_STATION_PORTS=8000,8005,8006,... + +# Docker User UID +# AZURACAST_PUID=1000 + +# Docker User GID +# AZURACAST_PGID=1000 + +# Advanced: Use Privileged Docker Settings +# Default: true +# AZURACAST_COMPOSE_PRIVILEGED=true + +# Additional Environment Variables +NGINX_TIMEOUT=1800 diff --git a/azuracast/README.md b/azuracast/README.md new file mode 100644 index 0000000..dc0036f --- /dev/null +++ b/azuracast/README.md @@ -0,0 +1,34 @@ +# AzuraCast + +Self-hosted web radio management suite. Runs stations, handles playlists, DJ accounts, streaming, and more. + +## Setup + +1. Copy `.env.example` to `.env`: + ``` + cp .env.example .env + ``` + +2. **Review `azuracast.env`** — This file contains database passwords and other internal config. Change `MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD` to secure values. + +3. Start: + ``` + docker compose up -d + ``` + + Or use the bundled helper script: + ``` + bash docker.sh install + ``` + +## Access + +- Web UI: `http://<host>:<AZURACAST_HTTP_PORT>` (default 8080) +- HTTPS: `https://<host>:<AZURACAST_HTTPS_PORT>` (default 8443) + +## What to Configure + +- **`.env`** — Set `AZURACAST_HTTP_PORT`, `AZURACAST_HTTPS_PORT`, and optionally `AZURACAST_PUID`/`AZURACAST_PGID` to match your host user. +- **`azuracast.env`** — Contains internal passwords and settings. Change at least the MySQL passwords. +- **Media mounts** — See `docker-compose.override.yml` for an example of mounting external media into station directories. +- **`docker.sh`** — The official AzuraCast management script for updates, backups, and maintenance tasks. diff --git a/azuracast/azuracast.env b/azuracast/azuracast.env new file mode 100644 index 0000000..8a45352 --- /dev/null +++ b/azuracast/azuracast.env @@ -0,0 +1,195 @@ +# This file was automatically generated by AzuraCast. +# You can modify it as necessary. To apply changes, restart the Docker containers. +# Remove the leading "#" symbol from lines to uncomment them. + +# The locale to use for CLI commands +# Valid options: en_US, cs_CZ, nl_NL, fr_FR, de_DE, el_GR, it_IT, ja_JP, ko_KR, nb_NO, pl_PL, pt_PT, pt_BR, ru_RU, zh_CN, es_ES, sv_SE, tr_TR, uk_UA +# Default: en_US +LANG=en_US + +# The application environment +# Valid options: Production, Testing, Development +APPLICATION_ENV=production + +# Manually modify the logging level +# This allows you to log debug-level errors temporarily (for problem-solving) +# or reduce the volume of logs that are produced by your installation, +# without needing to modify whether your installation is a production or +# development instance. +# Valid options: debug, info, notice, warning, error, critical, alert, emergency +# LOG_LEVEL= + +# Enable Custom Code Plugins +# Enable the composer "merge" functionality to combine the main application's +# composer.json file with any plugin composer files. This can have +# performance implications, so you should only use it if you use one or more +# plugins with their own Composer dependencies. +# Valid options: true, false +COMPOSER_PLUGIN_MODE=false + +# Minimum Port for Station Port Assignment +# Modify this if your stations are listening on nonstandard ports. +AUTO_ASSIGN_PORT_MIN=8000 + +# Maximum Port for Station Port Assignment +# Modify this if your stations are listening on nonstandard ports. +AUTO_ASSIGN_PORT_MAX=8499 + +# Show Detailed Slim Application Errors +# This allows you to debug Slim Application Errors you may encounter. By +# default, this is disabled to prevent users from seeing privileged +# information. Please report any Slim Application Error logs to the +# development team on GitHub. +# Valid options: true, false +SHOW_DETAILED_ERRORS=false + +# MariaDB Host +# Do not modify this after installation. +# Default: localhost +MYSQL_HOST=localhost + +# MariaDB Port +# Do not modify this after installation. +# Default: 3306 +MYSQL_PORT=3306 + +# MariaDB Username +# Do not modify this after installation. +# Default: azuracast +MYSQL_USER=azuracast + +# MariaDB Password +# Do not modify this after installation. +# Default: azur4c457 +MYSQL_PASSWORD=your_mysql_password_here + +# MariaDB Database Name +# Do not modify this after installation. +# Default: azuracast +MYSQL_DATABASE=azuracast + +# Auto-generate Random MariaDB Root Password +# Do not modify this after installation. +# MYSQL_RANDOM_ROOT_PASSWORD= + +# MariaDB Root Password +# Do not modify this after installation. +MYSQL_ROOT_PASSWORD=your_root_password_here + +# Enable MariaDB Slow Query Log +# Log slower queries to diagnose possible database issues. Only turn this on +# if needed. +# MYSQL_SLOW_QUERY_LOG= + +# MariaDB Maximum Connections +# Set the amount of allowed connections to the database. This value should be +# increased if you are seeing the "Too many connections" error in the logs. +# Default: 100 +# MYSQL_MAX_CONNECTIONS=100 + +# MariaDB InnoDB Buffer Pool Size +# The InnoDB buffer pool size controls how much data & indexes are kept in +# memory. Making sure that this value is as large as possible reduces the +# amount of disk IO. +# Default: 128M +# MYSQL_INNODB_BUFFER_POOL_SIZE=128M + +# MariaDB InnoDB Log File Size +# The InnoDB log file is used to achieve data durability in case of crashes +# or unexpected shutoffs and to allow the DB to better optimize IO for write +# operations. +# Default: 16M +# MYSQL_INNODB_LOG_FILE_SIZE=16M + +# Enable Redis +# Disable to use a flatfile cache instead of Redis. +# ENABLE_REDIS= + +# Redis Host +# Default: localhost +REDIS_HOST=localhost + +# Redis Port +# Default: 6379 +REDIS_PORT=6379 + +# Redis DB (Recommended to Use Default) +# REDIS_DB= + +# PHP Maximum POST File Size +# Default: 25M +# PHP_MAX_FILE_SIZE=25M + +# PHP Memory Limit +# Default: 128M +# PHP_MEMORY_LIMIT=128M + +# PHP Script Maximum Execution Time (Seconds) +# Default: 30 +# PHP_MAX_EXECUTION_TIME=30 + +# Short Sync Task Execution Time (Seconds) +# The maximum execution time (and lock timeout) for the 15-second, 1-minute +# and 5-minute synchronization tasks. +# SYNC_SHORT_EXECUTION_TIME= + +# Long Sync Task Execution Time (Seconds) +# The maximum execution time (and lock timeout) for the 1-hour +# synchronization task. +# SYNC_LONG_EXECUTION_TIME= + +# Now Playing Delay Time (Seconds) +# The delay (in seconds) between Now Playing checks for every station. +# Decrease for more frequent checks at the expense of performance; increase +# for less frequent checks but better performance (for large installations). +# NOW_PLAYING_DELAY_TIME= + +# Now Playing Max Concurrent Processes +# The maximum number of concurrent processes for now playing updates. +# Increasing this can help reduce the latency between updates now playing +# updates on large installations. +# NOW_PLAYING_MAX_CONCURRENT_PROCESSES= + +# Maximum PHP-FPM Worker Processes +# Default: 5 +# PHP_FPM_MAX_CHILDREN=5 + +# Enable Performance Profiling Extension +# Profiling data can be viewed by visiting +# http://your-azuracast-site/?SPX_KEY=dev&SPX_UI_URI=/. +# PROFILING_EXTENSION_ENABLED= + +# Profile Performance on All Requests +# This will have a significant performance impact on your installation. +# PROFILING_EXTENSION_ALWAYS_ON= + +# Profiling Extension HTTP Key +# The value for the "SPX_KEY" parameter for viewing profiling pages. +# PROFILING_EXTENSION_HTTP_KEY= + +# Profiling Extension IP Allow List +# Valid options: 127.0.0.1, * +# Default: * +# PROFILING_EXTENSION_HTTP_IP_WHITELIST=* + +# Nginx Max Client Body Size +# This is the total size any single request body can be. AzuraCast chunks its +# uploads into smaller file sizes, so this only applies when doing custom +# uploads via the API. Sizes should be listed in a format like "100K", +# "128M", "1G" for kilobytes, megabytes, and gigabytes respectively. +# Default: 50M +# NGINX_CLIENT_MAX_BODY_SIZE=50M + +# Automatically block common bots and crawlers +# If enabled, this will automatically download and update the Ultimate Nginx +# Bad Bot Blocker, which will block aggressive crawlers and other bots. +# Valid options: true, false +NGINX_BLOCK_BOTS=false + +# Enable web-based Docker image updates +# Default: true +# ENABLE_WEB_UPDATER=true + +# Extra Ubuntu packages to install upon startup +# Default: "Separate package names with a space. Packages will be installed during container startup." +# INSTALL_PACKAGES_ON_STARTUP="Separate package names with a space. Packages will be installed during container startup." diff --git a/azuracast/docker-compose.override.yml b/azuracast/docker-compose.override.yml new file mode 100644 index 0000000..3a00cde --- /dev/null +++ b/azuracast/docker-compose.override.yml @@ -0,0 +1,6 @@ +services: + web: + volumes: + - /srv/mus/:/var/azuracast/stations/cool_cats/media/ + - /srv/mus/:/var/azuracast/stations/night_ride/media/ + - /srv/mus/:/var/azuracast/stations/editors_cut/media/ diff --git a/azuracast/docker-compose.yml b/azuracast/docker-compose.yml new file mode 100644 index 0000000..3bae85a --- /dev/null +++ b/azuracast/docker-compose.yml @@ -0,0 +1,202 @@ +name: azuracast +services: + web: + container_name: azuracast + image: 'ghcr.io/azuracast/azuracast:${AZURACAST_VERSION:-latest}' + labels: + - com.centurylinklabs.watchtower.scope=azuracast + ports: + - '${AZURACAST_HTTP_PORT:-80}:${AZURACAST_HTTP_PORT:-80}' + - '${AZURACAST_HTTPS_PORT:-443}:${AZURACAST_HTTPS_PORT:-443}' + - '${AZURACAST_SFTP_PORT:-2022}:${AZURACAST_SFTP_PORT:-2022}' + - '8000:8000' + - '8005:8005' + - '8006:8006' + - '8010:8010' + - '8015:8015' + - '8016:8016' + - '8020:8020' + - '8025:8025' + - '8026:8026' + - '8030:8030' + - '8035:8035' + - '8036:8036' + - '8040:8040' + - '8045:8045' + - '8046:8046' + - '8050:8050' + - '8055:8055' + - '8056:8056' + - '8060:8060' + - '8065:8065' + - '8066:8066' + - '8070:8070' + - '8075:8075' + - '8076:8076' + - '8090:8090' + - '8095:8095' + - '8096:8096' + - '8100:8100' + - '8105:8105' + - '8106:8106' + - '8110:8110' + - '8115:8115' + - '8116:8116' + - '8120:8120' + - '8125:8125' + - '8126:8126' + - '8130:8130' + - '8135:8135' + - '8136:8136' + - '8140:8140' + - '8145:8145' + - '8146:8146' + - '8150:8150' + - '8155:8155' + - '8156:8156' + - '8160:8160' + - '8165:8165' + - '8166:8166' + - '8170:8170' + - '8175:8175' + - '8176:8176' + - '8180:8180' + - '8185:8185' + - '8186:8186' + - '8190:8190' + - '8195:8195' + - '8196:8196' + - '8200:8200' + - '8205:8205' + - '8206:8206' + - '8210:8210' + - '8215:8215' + - '8216:8216' + - '8220:8220' + - '8225:8225' + - '8226:8226' + - '8230:8230' + - '8235:8235' + - '8236:8236' + - '8240:8240' + - '8245:8245' + - '8246:8246' + - '8250:8250' + - '8255:8255' + - '8256:8256' + - '8260:8260' + - '8265:8265' + - '8266:8266' + - '8270:8270' + - '8275:8275' + - '8276:8276' + - '8280:8280' + - '8285:8285' + - '8286:8286' + - '8290:8290' + - '8295:8295' + - '8296:8296' + - '8300:8300' + - '8305:8305' + - '8306:8306' + - '8310:8310' + - '8315:8315' + - '8316:8316' + - '8320:8320' + - '8325:8325' + - '8326:8326' + - '8330:8330' + - '8335:8335' + - '8336:8336' + - '8340:8340' + - '8345:8345' + - '8346:8346' + - '8350:8350' + - '8355:8355' + - '8356:8356' + - '8360:8360' + - '8365:8365' + - '8366:8366' + - '8370:8370' + - '8375:8375' + - '8376:8376' + - '8380:8380' + - '8385:8385' + - '8386:8386' + - '8390:8390' + - '8395:8395' + - '8396:8396' + - '8400:8400' + - '8405:8405' + - '8406:8406' + - '8410:8410' + - '8415:8415' + - '8416:8416' + - '8420:8420' + - '8425:8425' + - '8426:8426' + - '8430:8430' + - '8435:8435' + - '8436:8436' + - '8440:8440' + - '8445:8445' + - '8446:8446' + - '8450:8450' + - '8455:8455' + - '8456:8456' + - '8460:8460' + - '8465:8465' + - '8466:8466' + - '8470:8470' + - '8475:8475' + - '8476:8476' + - '8480:8480' + - '8485:8485' + - '8486:8486' + - '8490:8490' + - '8495:8495' + - '8496:8496' + env_file: + - azuracast.env + - .env + volumes: + - 'station_data:/var/azuracast/stations' + - 'backups:/var/azuracast/backups' + - 'db_data:/var/lib/mysql' + - 'www_uploads:/var/azuracast/storage/uploads' + - 'shoutcast2_install:/var/azuracast/storage/shoutcast2' + - 'stereo_tool_install:/var/azuracast/storage/stereo_tool' + - 'rsas_install:/var/azuracast/storage/rsas' + - 'geolite_install:/var/azuracast/storage/geoip' + - 'sftpgo_data:/var/azuracast/storage/sftpgo' + - 'acme:/var/azuracast/storage/acme' + restart: unless-stopped + ulimits: + nofile: + soft: 65536 + hard: 65536 + logging: + options: + max-size: 1m + max-file: '5' + updater: + container_name: azuracast_updater + image: 'ghcr.io/azuracast/updater:latest' + restart: unless-stopped + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' + logging: + options: + max-size: 1m + max-file: '5' +volumes: + db_data: {} + acme: {} + shoutcast2_install: {} + stereo_tool_install: {} + rsas_install: {} + geolite_install: {} + sftpgo_data: {} + station_data: {} + www_uploads: {} + backups: {} diff --git a/azuracast/docker.sh b/azuracast/docker.sh new file mode 100755 index 0000000..a494cd3 --- /dev/null +++ b/azuracast/docker.sh @@ -0,0 +1,949 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2145,SC2178,SC2120,SC2162 + +PODMAN_MODE=0 + +# Docker and Docker Compose aliases +d() { + if [[ $PODMAN_MODE -ne 0 ]]; then + podman "$@" + else + docker "$@" + fi +} + +dc() { + if [[ $PODMAN_MODE -ne 0 ]]; then + podman-compose "$@" + else + if [[ $(docker compose version) ]]; then + docker compose "$@" + else + docker-compose "$@" + fi + fi +} + +# Functions to manage .env files +__dotenv= +__dotenv_file= +__dotenv_cmd=.env + +.env() { + REPLY=() + [[ $__dotenv_file || ${1-} == -* ]] || .env.--file .env || return + if declare -F -- ".env.${1-}" >/dev/null; then + .env."$@" + return + fi + return 64 +} + +.env.-f() { .env.--file "$@"; } + +.env.get() { + .env::arg "get requires a key" "$@" && + [[ "$__dotenv" =~ ^(.*(^|$'\n'))([ ]*)"$1="(.*)$ ]] && + REPLY=${BASH_REMATCH[4]%%$'\n'*} && REPLY=${REPLY%"${REPLY##*[![:space:]]}"} +} + +.env.parse() { + local line key + while IFS= read -r line; do + line=${line#"${line%%[![:space:]]*}"} # trim leading whitespace + line=${line%"${line##*[![:space:]]}"} # trim trailing whitespace + if [[ ! "$line" || "$line" == '#'* ]]; then continue; fi + if (($#)); then + for key; do + if [[ $key == "${line%%=*}" ]]; then + REPLY+=("$line") + break + fi + done + else + REPLY+=("$line") + fi + done <<<"$__dotenv" + ((${#REPLY[@]})) +} + +.env.export() { ! .env.parse "$@" || export "${REPLY[@]}"; } + +.env.set() { + .env::file load || return + local key saved=$__dotenv + while (($#)); do + key=${1#+} + key=${key%%=*} + if .env.get "$key"; then + REPLY=() + if [[ $1 == +* ]]; then + shift + continue # skip if already found + elif [[ $1 == *=* ]]; then + __dotenv=${BASH_REMATCH[1]}${BASH_REMATCH[3]}$1$'\n'${BASH_REMATCH[4]#*$'\n'} + else + __dotenv=${BASH_REMATCH[1]}${BASH_REMATCH[4]#*$'\n'} + continue # delete all occurrences + fi + elif [[ $1 == *=* ]]; then + __dotenv+="${1#+}"$'\n' + fi + shift + done + [[ $__dotenv == "$saved" ]] || .env::file save +} + +.env.puts() { echo "${1-}" >>"$__dotenv_file" && __dotenv+="$1"$'\n'; } + +.env.generate() { + .env::arg "key required for generate" "$@" || return + .env.get "$1" && return || REPLY=$("${@:2}") || return + .env::one "generate: ouptut of '${*:2}' has more than one line" "$REPLY" || return + .env.puts "$1=$REPLY" +} + +.env.--file() { + .env::arg "filename required for --file" "$@" || return + __dotenv_file=$1 + .env::file load || return + (($# < 2)) || .env "${@:2}" +} + +.env::arg() { [[ "${2-}" ]] || { + echo "$__dotenv_cmd: $1" >&2 + return 64 +}; } + +.env::one() { [[ "$2" != *$'\n'* ]] || .env::arg "$1"; } + +.env::file() { + local REPLY=$__dotenv_file + case "$1" in + load) + __dotenv= + ! [[ -f "$REPLY" ]] || __dotenv="$(<"$REPLY")"$'\n' || return + ;; + save) + if [[ -L "$REPLY" ]] && declare -F -- realpath.resolved >/dev/null; then + realpath.resolved "$REPLY" + fi + { [[ ! -f "$REPLY" ]] || cp -p "$REPLY" "$REPLY.bak"; } && + printf %s "$__dotenv" >"$REPLY.bak" && mv "$REPLY.bak" "$REPLY" + ;; + esac +} + +# Shortcut to convert semver version (x.yyy.zzz) into a comparable number. +version-number() { + echo "$@" | awk -F. '{ printf("%03d%03d%03d\n", $1,$2,$3); }' +} + +# Get the current release channel for AzuraCast +get-release-channel() { + local AZURACAST_VERSION="latest" + if [[ -f .env ]]; then + .env --file .env get AZURACAST_VERSION + AZURACAST_VERSION="${REPLY:-latest}" + fi + + echo "$AZURACAST_VERSION" +} + +get-release-branch-name() { + if [[ $(get-release-channel) == "stable" ]]; then + echo "stable" + else + echo "main" + fi +} + +# This is a general-purpose function to ask Yes/No questions in Bash, either +# with or without a default answer. It keeps repeating the question until it +# gets a valid answer. +ask() { + # https://djm.me/ask + local prompt default reply + + while true; do + + if [[ "${2:-}" == "Y" ]]; then + prompt="Y/n" + default=Y + elif [[ "${2:-}" == "N" ]]; then + prompt="y/N" + default=N + else + prompt="y/n" + default= + fi + + # Ask the question (not using "read -p" as it uses stderr not stdout) + echo -n "$1 [$prompt] " + + read reply + + # Default? + if [[ -z "$reply" ]]; then + reply=${default} + fi + + # Check if the reply is valid + case "$reply" in + Y* | y*) return 0 ;; + N* | n*) return 1 ;; + esac + + done +} + +# Generate a prompt to set an environment file value. +envfile-set() { + local VALUE INPUT + + .env --file .env + + .env get "$1" + VALUE=${REPLY:-$2} + + echo -n "$3 [$VALUE]: " + read INPUT + + VALUE=${INPUT:-$VALUE} + + .env set "${1}=${VALUE}" +} + +# +# Configure the ports used by AzuraCast. +# +setup-ports() { + envfile-set "AZURACAST_HTTP_PORT" "80" "Port to use for HTTP connections" + envfile-set "AZURACAST_HTTPS_PORT" "443" "Port to use for HTTPS connections" + envfile-set "AZURACAST_SFTP_PORT" "2022" "Port to use for SFTP connections" +} + +# +# Configure release mode settings. +# +setup-release() { + if [[ ! -f .env ]]; then + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/sample.env -o .env + fi + + local OLD_RELEASE_CHANNEL + .env --file .env get AZURACAST_VERSION + OLD_RELEASE_CHANNEL="${REPLY:-latest}" + + local AZURACAST_VERSION="${OLD_RELEASE_CHANNEL}" + + if [[ ! -z "${1}" ]]; then + echo "Setting release channel to the specific value: ${1}" + AZURACAST_VERSION="${1}" + elif [[ $AZURACAST_VERSION == "latest" ]]; then + if ask "Your current release channel is 'Rolling Release'. Switch to 'Stable' release channel?" N; then + AZURACAST_VERSION="stable" + fi + elif [[ $AZURACAST_VERSION == "stable" ]]; then + if ask "Your current release channel is 'Stable'. Switch to 'Rolling Release' release channel?" N; then + AZURACAST_VERSION="latest" + fi + else + if ask "Your current release channel is locked to a stable release, version '${OLD_RELEASE_CHANNEL}'. Switch to the 'Stable' release channel?" N; then + AZURACAST_VERSION="stable" + fi + fi + + .env --file .env set AZURACAST_VERSION=${AZURACAST_VERSION} + + if [[ $AZURACAST_VERSION != $OLD_RELEASE_CHANNEL ]]; then + if ask "You should update the Docker Utility Script after changing release channels. Automatically update it now?" Y; then + update-self + fi + fi +} + +check-install-requirements() { + local CURRENT_OS CURRENT_ARCH REQUIRED_COMMANDS SCRIPT_DIR + + set -e + + echo "Checking installation requirements for AzuraCast..." + + CURRENT_OS=$(uname -s) + if [[ $CURRENT_OS == "Linux" ]]; then + echo -en "\e[32m[PASS]\e[0m Operating System: ${CURRENT_OS}\n" + elif [[ $CURRENT_OS == "Darwin" ]]; then + echo -en "\e[32m[PASS]\e[0m Operating System: ${CURRENT_OS}\n" + else + echo -en "\e[41m[FAIL]\e[0m Operating System: ${CURRENT_OS}\n" + + echo " You are running an unsupported operating system." + echo " Automated AzuraCast installation is not currently supported on this" + echo " operating system." + exit 1 + fi + + CURRENT_ARCH=$(uname -m) + if [[ $CURRENT_ARCH == "x86_64" ]]; then + echo -en "\e[32m[PASS]\e[0m Architecture: ${CURRENT_ARCH}\n" + elif [[ $CURRENT_ARCH == "aarch64" ]]; then + echo -en "\e[32m[PASS]\e[0m Architecture: ${CURRENT_ARCH}\n" + else + echo -en "\e[41m[FAIL]\e[0m Architecture: ${CURRENT_ARCH}\n" + + echo " You are running an unsupported processor architecture." + echo " Automated AzuraCast installation is not currently supported on this " + echo " operating system." + exit 1 + fi + + REQUIRED_COMMANDS=(curl awk) + for COMMAND in "${REQUIRED_COMMANDS[@]}" ; do + if [[ $(command -v "$COMMAND") ]]; then + echo -en "\e[32m[PASS]\e[0m Command Present: ${COMMAND}\n" + else + echo -en "\e[41m[FAIL]\e[0m Command Present: ${COMMAND}\n" + + echo " ${COMMAND} does not appear to be installed." + echo " Install ${COMMAND} using your host's package manager," + echo " then continue installing using this script." + exit 1 + fi + done + + if [[ $EUID -ne 0 ]]; then + if [[ $(command -v sudo) ]]; then + echo -en "\e[32m[PASS]\e[0m User Permissions\n" + else + echo -en "\e[41m[FAIL]\e[0m User Permissions\n" + + echo " You are not currently the root user, and " + echo " 'sudo' does not appear to be installed." + echo " Install sudo using your host's package manager," + echo " then continue installing using this script." + exit 1 + fi + else + echo -en "\e[32m[PASS]\e[0m User Permissions\n" + fi + + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + if [[ $SCRIPT_DIR == "/var/azuracast" ]]; then + echo -en "\e[32m[PASS]\e[0m Installation Directory\n" + else + echo -en "\e[93m[WARN]\e[0m Installation Directory\n" + echo " AzuraCast is not installed in /var/azuracast, as is recommended" + echo " for most installations. This will not prevent AzuraCast from" + echo " working, but you will need to update any instructions in our" + echo " documentation to reflect your current directory:" + echo " $SCRIPT_DIR" + fi + + echo -en "\e[32m[PASS]\e[0m All requirements met!\n" + + set +e +} + +install-docker() { + set -e + + CURRENT_OS=$(uname -s) + if [[ $CURRENT_OS != "Linux" ]]; then + echo "The automatic Docker installation can only take place on Linux." + echo "Install Docker Desktop for your operating system." + exit 1 + fi + + curl -fsSL get.docker.com -o get-docker.sh + sh get-docker.sh + rm get-docker.sh + + if [[ $EUID -ne 0 ]]; then + sudo usermod -aG docker "$(whoami)" + + echo "You must log out or restart to apply necessary Docker permissions changes." + echo "Restart, then continue installing using this script." + exit 1 + fi + + set +e +} + +install-docker-compose() { + set -e + local DOCKER_COMPOSE_VERSION + echo "Installing Docker Compose..." + + CURRENT_OS=$(uname -s) + if [[ $CURRENT_OS != "Linux" ]]; then + echo "The automatic Docker installation can only take place on Linux." + echo "Install Docker Desktop for your operating system." + exit 1 + fi + + DOCKER_COMPOSE_VERSION=$(version-number $(docker compose version --short || echo "0.0.0")) + if (( $DOCKER_COMPOSE_VERSION >= $(version-number "5.0.0") )); then + echo "Docker Compose is already installed and newer than version 5.0.0. No update needed." + set +e + else + curl -fsSL -o docker-compose https://github.com/docker/compose/releases/download/v5.0.0/docker-compose-linux-$(uname -m) + + ARCHITECTURE=amd64 + if [ "$(uname -m)" = "aarch64" ]; then + ARCHITECTURE=arm64 + fi + curl -fsSL -o docker-compose-switch https://github.com/docker/compose-switch/releases/download/v1.0.5/docker-compose-linux-${ARCHITECTURE} + + if [[ $EUID -ne 0 ]]; then + sudo chmod a+x ./docker-compose + sudo chmod a+x ./docker-compose-switch + + sudo mv ./docker-compose /usr/libexec/docker/cli-plugins/docker-compose + sudo mv ./docker-compose-switch /usr/local/bin/docker-compose + else + chmod a+x ./docker-compose + chmod a+x ./docker-compose-switch + + mv ./docker-compose /usr/libexec/docker/cli-plugins/docker-compose + mv ./docker-compose-switch /usr/local/bin/docker-compose + fi + + echo "Docker Compose updated!" + set +e + fi +} + +run-installer() { + local AZURACAST_RELEASE_BRANCH + AZURACAST_RELEASE_BRANCH=$(get-release-branch-name) + + if [[ ! -f .env ]]; then + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/sample.env -o .env + fi + if [[ ! -f azuracast.env ]]; then + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/azuracast.sample.env -o azuracast.env + fi + if [[ ! -f docker-compose.yml ]]; then + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/docker-compose.sample.yml -o docker-compose.yml + fi + + touch docker-compose.new.yml + + local dc_config_test=$(dc -f docker-compose.new.yml config 2>/dev/null) + if [ $? -ne 0 ]; then + if ask "Docker Compose needs to be updated to continue. Update to latest version?" Y; then + install-docker-compose + fi + fi + + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/docker-compose.installer.yml -o docker-compose.installer.yml + + dc -p azuracast_installer -f docker-compose.installer.yml pull + dc -p azuracast_installer -f docker-compose.installer.yml run --rm installer install "$@" + + rm docker-compose.installer.yml +} + +# +# Run the initial installer of Docker and AzuraCast. +# Usage: ./docker.sh install +# +install() { + check-install-requirements + + if [[ $PODMAN_MODE -ne 0 ]]; then + echo "Podman was detected and will be used instead of Docker..." + + if [[ $(command -v podman-compose) ]]; then + echo "Podman-compose is installed!" + else + echo "Podman mode is active, but podman-compose is not found." + echo "Install it by following the instructions on this page:" + echo "https://github.com/containers/podman-compose" + exit 1 + fi + else + if [[ $(command -v docker) && $(docker --version) ]]; then + echo "Docker is already installed! Continuing..." + else + if ask "Docker does not appear to be installed. Install Docker now?" Y; then + install-docker + fi + fi + + if [[ $(docker compose version) ]]; then + echo "Docker Compose v2 is already installed. Continuing..." + else + if [[ $(command -v docker-compose) ]]; then + echo "Docker Compose is already installed. Continuing..." + else + if ask "Docker Compose does not appear to be installed. Install Docker Compose now?" Y; then + install-docker-compose + fi + fi + fi + fi + + setup-release + + run-installer "$@" + + # Installer creates a file at docker-compose.new.yml; copy it to the main spot. + if [[ -s docker-compose.new.yml ]]; then + if [[ -f docker-compose.yml ]]; then + rm docker-compose.yml + fi + + mv docker-compose.new.yml docker-compose.yml + fi + + # If this script is running as a non-root user, set the PUID/PGID in the environment vars appropriately. + if [[ $EUID -ne 0 ]]; then + .env --file .env set AZURACAST_PUID="$(id -u)" + .env --file .env set AZURACAST_PGID="$(id -g)" + fi + + if [[ $PODMAN_MODE -ne 0 ]]; then + .env --file .env set AZURACAST_PODMAN_MODE=true + fi + + dc pull + + dc run --rm web -- azuracast_install "$@" + dc up -d + exit +} + +install-dev() { + if [[ $(command -v docker) && $(docker --version) ]]; then + echo "Docker is already installed! Continuing..." + else + if ask "Docker does not appear to be installed. Install Docker now?" Y; then + install-docker + fi + fi + + if [[ $(docker compose version) ]]; then + echo "Docker Compose v2 is already installed. Continuing..." + else + if [[ $(command -v docker-compose) ]]; then + echo "Docker Compose is already installed. Continuing..." + else + if ask "Docker Compose does not appear to be installed. Install Docker Compose now?" Y; then + install-docker-compose + fi + fi + fi + + if [[ ! -f docker-compose.yml ]]; then + cp docker-compose.sample.yml docker-compose.yml + fi + if [[ ! -f docker-compose.override.yml ]]; then + cp docker-compose.dev.yml docker-compose.override.yml + fi + if [[ ! -f .env ]]; then + cp dev.env .env + fi + if [[ ! -f azuracast.env ]]; then + cp azuracast.dev.env azuracast.env + + echo "Customize azuracast.env file now before continuing. Re-run this command to continue installation." + exit + fi + + # If this script is running as a non-root user, set the PUID/PGID in the environment vars appropriately. + if [[ $EUID -ne 0 ]]; then + .env --file .env set AZURACAST_PUID="$(id -u)" + .env --file .env set AZURACAST_PGID="$(id -g)" + fi + + if [[ $PODMAN_MODE -ne 0 ]]; then + .env --file .env set AZURACAST_PODMAN_MODE=true + fi + + dc build + dc run --rm web -- azuracast_dev_install "$@" + dc up -d + exit +} + +# +# Update the Docker images and codebase. +# Usage: ./docker.sh update +# +update() { + echo "[NOTICE] Before you continue, please make sure you have a recent snapshot of your system and or backed it up." + if ask "Are you ready to continue with the update?" Y; then + + # Check for a new Docker Utility Script. + local AZURACAST_RELEASE_BRANCH + AZURACAST_RELEASE_BRANCH=$(get-release-branch-name) + + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/docker.sh -o docker.new.sh + + local UTILITY_FILES_MATCH + UTILITY_FILES_MATCH="$( + cmp --silent docker.sh docker.new.sh + echo $? + )" + + local UPDATE_UTILITY=0 + if [[ ${UTILITY_FILES_MATCH} -ne 0 ]]; then + if ask "The Docker Utility Script has changed since your version. Update to latest version?" Y; then + UPDATE_UTILITY=1 + fi + fi + + if [[ ${UPDATE_UTILITY} -ne 0 ]]; then + mv docker.new.sh docker.sh + chmod a+x docker.sh + + echo "A new Docker Utility Script has been downloaded." + echo "Please re-run the update process to continue." + exit + else + rm docker.new.sh + fi + + # Check Docker version. + if [[ $PODMAN_MODE -eq 0 ]]; then + DOCKER_VERSION=$(docker version -f "{{.Server.Version}}") + DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 1) + + if [ "${DOCKER_VERSION_MAJOR}" -ge 20 ]; then + echo "Docker server (version ${DOCKER_VERSION}) meets minimum version requirements." + else + if ask "Docker is out of date on this server. Attempt automatic upgrade?" Y; then + install-docker + install-docker-compose + fi + fi + fi + + run-installer --update "$@" + + # Check for updated Docker Compose config. + local COMPOSE_FILES_MATCH + + if [[ ! -s docker-compose.new.yml ]]; then + curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/docker-compose.sample.yml -o docker-compose.new.yml + fi + + COMPOSE_FILES_MATCH="$( + cmp --silent docker-compose.yml docker-compose.new.yml + echo $? + )" + + if [[ ${COMPOSE_FILES_MATCH} -ne 0 ]]; then + dc -f docker-compose.new.yml pull + dc down --timeout 60 + + cp docker-compose.yml docker-compose.backup.yml + mv docker-compose.new.yml docker-compose.yml + else + rm docker-compose.new.yml + + dc pull + dc down --timeout 60 + fi + + dc run --rm web -- azuracast_update "$@" + dc up -d + + if ask "Clean up all stopped Docker containers and images to save space?" Y; then + d system prune -f + fi + + echo "Update complete!" + fi + exit +} + +# +# Update this Docker utility script. +# Usage: ./docker.sh update-self +# +update-self() { + local AZURACAST_RELEASE_BRANCH + AZURACAST_RELEASE_BRANCH=$(get-release-branch-name) + + curl -H 'Cache-Control: no-cache, no-store' -fsSL \ + https://raw.githubusercontent.com/AzuraCast/AzuraCast/$AZURACAST_RELEASE_BRANCH/docker.sh?$(date +%s) \ + -o docker.sh + chmod a+x docker.sh + + echo "New Docker utility script downloaded." + echo "You can now re-run any previous command with the updated utility script." + exit +} + +# +# Run a CLI command inside the Docker container. +# Usage: ./docker.sh cli [command] +# +cli() { + dc exec --user="azuracast" web azuracast_cli "$@" + exit +} + +# +# Enter the bash terminal of the running web container. +# Usage: ./docker.sh bash +# +bash() { + dc exec --user="azuracast" web bash + exit +} + +# +# Enter the MariaDB database management terminal with the correct credentials. +# +db() { + dc exec web azuracast_db + exit +} + +# +# Back up the Docker volumes to a .tar.gz file. +# Usage: +# ./docker.sh backup [/custom/backup/dir/custombackupname.zip] +# +backup() { + local BACKUP_PATH BACKUP_DIR BACKUP_FILENAME BACKUP_EXT + BACKUP_PATH=$(readlink -f ${1:-"./backup.tar.gz"}) + BACKUP_DIR=$(dirname -- "$BACKUP_PATH") + BACKUP_FILENAME=$(basename -- "$BACKUP_PATH") + BACKUP_EXT="${BACKUP_FILENAME##*.}" + shift + + # Prepare permissions + if [[ $EUID -ne 0 ]]; then + .env --file .env set AZURACAST_PUID="$(id -u)" + .env --file .env set AZURACAST_PGID="$(id -g)" + fi + + dc exec --user="azuracast" web azuracast_cli azuracast:backup "/var/azuracast/backups/${BACKUP_FILENAME}" "$@" + + # Move from Docker volume to local filesystem + d run --rm -v "azuracast_backups:/backup_src" \ + -v "$BACKUP_DIR:/backup_dest" \ + busybox mv "/backup_src/${BACKUP_FILENAME}" "/backup_dest/${BACKUP_FILENAME}" + + echo "Backup completed." + exit +} + +# +# Restore an AzuraCast backup into Docker. +# Usage: +# ./docker.sh restore [/custom/backup/dir/custombackupname.zip] +# +restore() { + if [[ ! -f .env ]] || [[ ! -f azuracast.env ]]; then + echo "AzuraCast hasn't been installed yet on this server." + echo "You should run './docker.sh install' first before restoring." + exit 1 + fi + + if ask "Restoring will remove any existing AzuraCast installation data, replacing it with your backup. Continue?" Y; then + if [[ $1 != "" ]]; then + local BACKUP_PATH BACKUP_DIR BACKUP_FILENAME BACKUP_EXT + BACKUP_PATH=$(readlink -f ${1:-"./backup.tar.gz"}) + BACKUP_DIR=$(dirname -- "$BACKUP_PATH") + BACKUP_FILENAME=$(basename -- "$BACKUP_PATH") + BACKUP_EXT="${BACKUP_FILENAME##*.}" + shift + + if [[ ! -f ${BACKUP_PATH} ]]; then + echo "File '${BACKUP_PATH}' does not exist. Nothing to restore." + exit 1 + fi + + dc down + + # Remove most AzuraCast volumes but preserve some essential ones. + d volume rm -f $(d volume ls | grep 'azuracast' | grep -v 'station\|install' | awk 'NR>1 {print $2}') + d volume create azuracast_backups + + # Move from local filesystem to Docker volume + d run --rm -v "$BACKUP_DIR:/backup_src" \ + -v "azuracast_backups:/backup_dest" \ + busybox mv "/backup_src/${BACKUP_FILENAME}" "/backup_dest/${BACKUP_FILENAME}" + + # Prepare permissions + if [[ $EUID -ne 0 ]]; then + .env --file .env set AZURACAST_PUID="$(id -u)" + .env --file .env set AZURACAST_PGID="$(id -g)" + fi + + dc run --rm web -- azuracast_restore "/var/azuracast/backups/${BACKUP_FILENAME}" "$@" + + # Move file back from volume to local filesystem + d run --rm -v "azuracast_backups:/backup_src" \ + -v "$BACKUP_DIR:/backup_dest" \ + busybox mv "/backup_src/${BACKUP_FILENAME}" "/backup_dest/${BACKUP_FILENAME}" + + dc down --timeout 30 + dc up -d + else + dc down + + # Remove most AzuraCast volumes but preserve some essential ones. + d volume rm -f $(d volume ls | grep 'azuracast' | grep -v 'station\|backups\|install' | awk 'NR>1 {print $2}') + + dc run --rm web -- azuracast_restore "$@" + + dc down --timeout 30 + dc up -d + fi + fi + exit +} + +# +# Restore the Docker volumes from a legacy backup format .tar.gz file. +# Usage: +# ./docker.sh restore [/custom/backup/dir/custombackupname.tar.gz] +# +restore-legacy() { + local APP_BASE_DIR BACKUP_PATH BACKUP_DIR BACKUP_FILENAME + + APP_BASE_DIR=$(pwd) + + BACKUP_PATH=${1:-"./backup.tar.gz"} + BACKUP_DIR=$(cd "$(dirname "$BACKUP_PATH")" && pwd) + BACKUP_FILENAME=$(basename "$BACKUP_PATH") + + cd "$APP_BASE_DIR" || exit + + if [ -f "$BACKUP_PATH" ]; then + dc down --timeout 30 + + d volume rm azuracast_db_data azuracast_station_data + d volume create azuracast_db_data + d volume create azuracast_station_data + + d run --rm -v "$BACKUP_DIR:/backup" \ + -v azuracast_db_data:/azuracast/db \ + -v azuracast_station_data:/azuracast/stations \ + busybox tar zxvf "/backup/$BACKUP_FILENAME" + + dc up -d + else + echo "File $BACKUP_PATH does not exist in this directory. Nothing to restore." + exit 1 + fi + + exit +} + +# +# Stop all Docker containers and remove related volumes. +# Usage: ./docker.sh uninstall +# +uninstall() { + if ask "This operation is destructive and will wipe your existing Docker containers. Continue?" N; then + + dc down -v + dc rm -f + d volume prune -f + + echo "All AzuraCast Docker containers and volumes were removed." + echo "To remove *all* Docker containers and volumes, run:" + echo " docker stop \$(docker ps -a -q)" + echo " docker rm \$(docker ps -a -q)" + echo " docker volume prune -f" + echo "" + fi + + exit +} + +# +# Roll back to a specific stable release version. +# +rollback() { + local AZURACAST_ROLLBACK_VERSION + AZURACAST_ROLLBACK_VERSION="$1" + + if [[ -z "$AZURACAST_ROLLBACK_VERSION" ]]; then + echo "No version specified. Specify a version, like 0.19.0." + exit 1 + fi + + echo "[NOTICE] Before you continue, please make sure you have a recent snapshot of your system and or backed it up." + if ask "Are you ready to continue with the rollback?" Y; then + dc exec --user="azuracast" web azuracast_cli azuracast:setup:rollback "${AZURACAST_ROLLBACK_VERSION}" + dc down --timeout 60 + + .env --file .env set AZURACAST_VERSION=${AZURACAST_ROLLBACK_VERSION} + + dc pull + dc run --rm web -- azuracast_update + dc up -d + + if ask "Clean up all stopped Docker containers and images to save space?" Y; then + d system prune -f + fi + + echo "Rollback complete. Your installation has been returned to stable version '${AZURACAST_ROLLBACK_VERSION}'." + echo "To return to the regular update channels, run:" + echo " ./docker.sh setup-release" + echo " " + fi + exit +} + +# +# LetsEncrypt: Now managed via the Web UI. +# +setup-letsencrypt() { + echo "LetsEncrypt is now managed from within the web interface." + echo "You can manage it via the 'Administration' panel, then 'System Settings'." + echo "Under 'Services' you will find the LetsEncrypt settings." +} + +letsencrypt-create() { + setup-letsencrypt + exit +} + +# +# Utility script to facilitate switching ports. +# Usage: ./docker.sh change-ports +# +change-ports() { + setup-ports + + dc down --timeout 60 + dc up -d +} + +# +# Helper scripts for basic Docker Compose functions +# +up() { + echo "Starting up AzuraCast services..." + dc up -d +} + +down() { + echo "Shutting down AzuraCast services..." + dc down --timeout 60 +} + +restart() { + down + up +} + +# Ensure we're in the same directory as this script. +cd "$( dirname "${BASH_SOURCE[0]}" )" || exit + +# Podman support +if [[ $(command -v podman) ]]; then + PODMAN_MODE=1 +fi + +"$@" diff --git a/flood/README.md b/flood/README.md new file mode 100644 index 0000000..00a0636 --- /dev/null +++ b/flood/README.md @@ -0,0 +1,20 @@ +# Flood + +Web UI for rTorrent. + +## Setup + +1. Start: + ``` + docker compose up -d + ``` + +## Access + +- Web UI: `http://<host>:9091` + +## What to Configure + +- **rTorrent socket** — The volume `- /home/tyler/rtorrent/.session:/home/tyler/rtorrent/.session` must point to your rTorrent session directory (where `rpc.socket` lives). Update the host path if your rTorrent setup differs. +- **Downloads volume** — `- /srv/torrents:/srv/torrents` mounts your torrent download directory. Update this to match your setup. +- **User UID/GID** — The `user: "1000:1000"` line should match your rTorrent user. Change it if rTorrent runs as a different user. diff --git a/flood/docker-compose.yml b/flood/docker-compose.yml new file mode 100644 index 0000000..59dbc38 --- /dev/null +++ b/flood/docker-compose.yml @@ -0,0 +1,17 @@ +services: + flood: + image: jesec/flood:latest + container_name: flood + user: "1000:1000" + command: --port 9091 + ports: + - "9091:9091" + environment: + HOME: /config + volumes: + # Mount rtorrent session directory (contains rpc.socket) + - /home/tyler/rtorrent/.session:/home/tyler/rtorrent/.session + - ./flood-config:/config + # Mount RAID storage for downloads + - /srv/torrents:/srv/torrents + restart: unless-stopped diff --git a/glance/.env.example b/glance/.env.example new file mode 100644 index 0000000..6d1e7c4 --- /dev/null +++ b/glance/.env.example @@ -0,0 +1,3 @@ +# Variables defined here will be available to use anywhere in the config with the syntax ${MY_SECRET_TOKEN} +# Note: making changes to this file requires re-running docker compose up +MY_SECRET_TOKEN=your_secret_token_here diff --git a/glance/README.md b/glance/README.md new file mode 100644 index 0000000..e1c6168 --- /dev/null +++ b/glance/README.md @@ -0,0 +1,28 @@ +# Glance + +A highly customizable personal dashboard page. + +## Setup + +1. Copy `.env.example` to `.env` and set your secret token: + ``` + cp .env.example .env + ``` + +2. Place your `glance.yml` config file in `./config/`. + +3. Start: + ``` + docker compose up -d + ``` + +## Access + +- Dashboard: `http://<host>:7575` + +## What to Configure + +- **`.env`** — Set `MY_SECRET_TOKEN` to a random string. This is used in the Glance config to reference secrets (e.g. API tokens for widgets). +- **Config** — You need a `glance.yml` in `./config/`. See the [Glance docs](https://github.com/glanceapp/glance) for the config format. +- **Assets** — Static assets (images, CSS) go in `./assets/` and are served at `/assets/`. +- **System mounts** — The compose file mounts `/srv` and `/home` as read-only. Remove or adjust these if your dashboard doesn't need filesystem access. diff --git a/glance/docker-compose.yml b/glance/docker-compose.yml new file mode 100644 index 0000000..51825a4 --- /dev/null +++ b/glance/docker-compose.yml @@ -0,0 +1,16 @@ +services: + glance: + container_name: glance + image: glanceapp/glance + restart: unless-stopped + volumes: + - ./config:/app/config + - ./assets:/app/assets + - /etc/localtime:/etc/localtime:ro + - /srv:/raid:ro + - /home:/root:ro + # Optionally, also mount docker socket if you want to use the docker containers widget + # - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - 7575:8080 + env_file: .env diff --git a/glances/README.md b/glances/README.md new file mode 100644 index 0000000..bdef3f3 --- /dev/null +++ b/glances/README.md @@ -0,0 +1,21 @@ +# Glances + +Real-time system monitoring. + +## Setup + +1. Start: + ``` + docker compose up -d + ``` + +## Access + +- Web UI: `http://<host>:61208` + +## What to Configure + +- **`glances.conf`** — Configures which filesystems to hide from monitoring. Edit this to match your system. +- **Time zone** — Change `TZ=America/Los_Angeles` to your timezone. +- **Docker socket** — Mounts `/var/run/docker.sock` to show container stats. Remove if not needed. +- **System mounts** — `/srv` and `/home` are mounted as read-only for disk usage stats. Adjust or remove as needed. diff --git a/glances/docker-compose.yml b/glances/docker-compose.yml new file mode 100644 index 0000000..545dcb1 --- /dev/null +++ b/glances/docker-compose.yml @@ -0,0 +1,16 @@ + +services: + monitoring: + image: nicolargo/glances:latest + restart: unless-stopped + pid: host + ports: + - "61208:61208/tcp" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./glances.conf:/etc/glances/glances.conf + - /srv:/raid:ro + - /home:/root:ro + environment: + - "GLANCES_OPT=-w" + - "TZ=America/Los_Angeles" diff --git a/glances/glances.conf b/glances/glances.conf new file mode 100644 index 0000000..7dc36e1 --- /dev/null +++ b/glances/glances.conf @@ -0,0 +1,2 @@ +[fs] +hide=loop.*,/dev/loop.*,/etc.* diff --git a/grafana/README.md b/grafana/README.md new file mode 100644 index 0000000..4e0a53d --- /dev/null +++ b/grafana/README.md @@ -0,0 +1,23 @@ +# Grafana + Prometheus + +Metrics visualization dashboard with Prometheus as the data source. + +## Setup + +1. Edit `prometheus.yml` to add your scrape targets. + +2. Start: + ``` + docker compose up -d + ``` + +## Access + +- Grafana: `http://<host>:3000` + - Default login: `admin` / `admin` (change on first login) + +## What to Configure + +- **`prometheus.yml`** — Add your scrape targets (node_exporter, nginx_exporter, etc.) before starting. The file currently contains example targets. +- **Grafana** — On first login, add Prometheus as a data source at `http://prometheus:9090`. +- **Data persistence** — Both `prometheus_data` and `grafana_data` are Docker volumes. They persist across restarts and stack upgrades. diff --git a/grafana/docker-compose.yml b/grafana/docker-compose.yml new file mode 100644 index 0000000..f9be4fd --- /dev/null +++ b/grafana/docker-compose.yml @@ -0,0 +1,21 @@ +services: + prometheus: + image: prom/prometheus + container_name: prometheus + restart: unless-stopped + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + + grafana: + image: grafana/grafana + container_name: grafana + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - grafana_data:/var/lib/grafana + +volumes: + prometheus_data: + grafana_data: diff --git a/grafana/prometheus.yml b/grafana/prometheus.yml new file mode 100644 index 0000000..2198130 --- /dev/null +++ b/grafana/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + +scrape_configs: + # Add your own scrape targets. Examples: + # - job_name: node_exporter + # static_configs: + # - targets: ['<your-server-ip>:9100'] + # + # - job_name: nginx_exporter + # static_configs: + # - targets: ['<your-server-ip>:9113'] + # + # - job_name: pihole + # static_configs: + # - targets: ['<your-pihole-ip>:9617'] diff --git a/immich/.env.example b/immich/.env.example new file mode 100644 index 0000000..a7e4255 --- /dev/null +++ b/immich/.env.example @@ -0,0 +1,24 @@ +# You can find documentation for all the supported env variables at https://docs.immich.app/install/environment-variables + +# The location where your uploaded files are stored +UPLOAD_LOCATION=/srv/photos + +# The location where your database files are stored. Network shares are not supported for the database +DB_DATA_LOCATION=./postgres + +# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List +TZ=America/Los_Angeles + +# The Immich version to use. You can pin this to a specific version like "v2.1.0" +IMMICH_VERSION=v2 + +# Connection secret for postgres. You should change it to a random password +# Please use only the characters `A-Za-z0-9`, without special characters or spaces +DB_PASSWORD=your_db_password_here + +# The values below this line do not need to be changed +################################################################################### +DB_USERNAME=postgres +DB_DATABASE_NAME=immich + +IMMICH_VERSION=v3 diff --git a/immich/README.md b/immich/README.md new file mode 100644 index 0000000..788372a --- /dev/null +++ b/immich/README.md @@ -0,0 +1,31 @@ +# Immich + +Self-hosted photo and video management solution. + +## Setup + +1. Copy `.env.example` to `.env`: + ``` + cp .env.example .env + ``` + +2. Edit `.env`: + - Set `DB_PASSWORD` to a secure random string. + - Set `UPLOAD_LOCATION` to where you want photos stored. + - Set `DB_DATA_LOCATION` to where you want the database stored (avoid network shares). + - Set `TZ` to your timezone. + +3. Start: + ``` + docker compose up -d + ``` + +## Access + +- Web UI: `http://<host>:2283` + +## What to Configure + +- **`.env`** — All primary configuration lives here. See the [Immich docs](https://docs.immich.app/install/environment-variables) for all options. +- **Hardware acceleration** — Uncomment the `extends` sections in `docker-compose.yml` for transcoding/ML hardware acceleration. +- **`backup.sh`** — Example borg backup script for database and photo backups. Edit the paths and remote host to match your setup before using. diff --git a/immich/backup.sh b/immich/backup.sh new file mode 100755 index 0000000..91770a9 --- /dev/null +++ b/immich/backup.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# Paths +UPLOAD_LOCATION="/srv/photos" +BACKUP_PATH="/path/to/local/immich-backup" +REMOTE_HOST="user@remote-host" +REMOTE_BACKUP_PATH="/path/to/remote/immich" + + +### Local + +# Backup Immich database +docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "$UPLOAD_LOCATION"/database-backup/immich-database.sql +# For deduplicating backup programs such as Borg or Restic, compressing the content can increase backup size by making it harder to deduplicate. If you are using a different program or still prefer to compress, you can use the following command instead: +# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz + +### Append to local Borg repository +borg create "$BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/ +borg prune --keep-weekly=4 --keep-monthly=3 "$BACKUP_PATH"/immich-borg +borg compact "$BACKUP_PATH"/immich-borg + + +### Append to remote Borg repository +borg create "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/ +borg prune --keep-weekly=4 --keep-monthly=3 "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg +borg compact "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg diff --git a/immich/docker-compose.yml b/immich/docker-compose.yml new file mode 100755 index 0000000..2dd1a7f --- /dev/null +++ b/immich/docker-compose.yml @@ -0,0 +1,76 @@ +# +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose +# +# Make sure to use the docker-compose.yml of the current release: +# +# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +# +# The compose file on main may not be compatible with the latest release. + +name: immich + +services: + immich-server: + container_name: immich_server + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.transcoding.yml + # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + volumes: + # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file + - ${UPLOAD_LOCATION}:/data + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ports: + - '2283:2283' + depends_on: + - redis + - database + restart: always + healthcheck: + disable: false + + redis: + container_name: immich_redis + image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 + healthcheck: + test: redis-cli ping || exit 1 + restart: always + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable + volumes: + - model-cache:/cache + env_file: + - .env + restart: always + healthcheck: + disable: false + + database: + container_name: immich_postgres + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs + # DB_STORAGE_TYPE: 'HDD' + volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file + - ${DB_DATA_LOCATION}:/var/lib/postgresql/data + shm_size: 128mb + restart: always + healthcheck: + disable: false + +volumes: + model-cache: diff --git a/invidious/README.md b/invidious/README.md new file mode 100644 index 0000000..fbfa29f --- /dev/null +++ b/invidious/README.md @@ -0,0 +1,28 @@ +# Invidious + +Self-hosted YouTube frontend. No ads, no tracking. + +## Setup + +1. Generate required keys: + ``` + docker compose run --rm invidious --generate-hmac-key + docker compose run --rm invidious --generate-api-key + ``` + +2. Set the generated keys as environment variables and start: + ``` + INVIDIOUS_HMAC_KEY=<generated> INVIDIOUS_COMPANION_KEY=<generated> docker compose up -d + ``` + + Or add them to an `.env` file (create one in this directory). + +## Access + +- Web UI: `http://<host>:3000` + +## What to Configure + +- **Keys** — `INVIDIOUS_HMAC_KEY` and `INVIDIOUS_COMPANION_KEY` must be set. Without them, Invidious will not work. +- **Database password** — Defaults to `kemal`. Change via `POSTGRES_PASSWORD` env var for production. +- **Database init** — `docker/init-invidious-db.sh` and `config/sql/` are used for first-time database setup. No changes needed unless you customize the schema. diff --git a/invidious/config/sql/annotations.sql b/invidious/config/sql/annotations.sql new file mode 100644 index 0000000..3705829 --- /dev/null +++ b/invidious/config/sql/annotations.sql @@ -0,0 +1,12 @@ +-- Table: public.annotations + +-- DROP TABLE public.annotations; + +CREATE TABLE IF NOT EXISTS public.annotations +( + id text NOT NULL, + annotations xml, + CONSTRAINT annotations_id_key UNIQUE (id) +); + +GRANT ALL ON TABLE public.annotations TO current_user; diff --git a/invidious/config/sql/channel_videos.sql b/invidious/config/sql/channel_videos.sql new file mode 100644 index 0000000..cd4e0ff --- /dev/null +++ b/invidious/config/sql/channel_videos.sql @@ -0,0 +1,30 @@ +-- Table: public.channel_videos + +-- DROP TABLE public.channel_videos; + +CREATE TABLE IF NOT EXISTS public.channel_videos +( + id text NOT NULL, + title text, + published timestamp with time zone, + updated timestamp with time zone, + ucid text, + author text, + length_seconds integer, + live_now boolean, + premiere_timestamp timestamp with time zone, + views bigint, + CONSTRAINT channel_videos_id_key UNIQUE (id) +); + +GRANT ALL ON TABLE public.channel_videos TO current_user; + +-- Index: public.channel_videos_ucid_idx + +-- DROP INDEX public.channel_videos_ucid_idx; + +CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx + ON public.channel_videos + USING btree + (ucid COLLATE pg_catalog."default"); + diff --git a/invidious/config/sql/channels.sql b/invidious/config/sql/channels.sql new file mode 100644 index 0000000..55772da --- /dev/null +++ b/invidious/config/sql/channels.sql @@ -0,0 +1,25 @@ +-- Table: public.channels + +-- DROP TABLE public.channels; + +CREATE TABLE IF NOT EXISTS public.channels +( + id text NOT NULL, + author text, + updated timestamp with time zone, + deleted boolean, + subscribed timestamp with time zone, + CONSTRAINT channels_id_key UNIQUE (id) +); + +GRANT ALL ON TABLE public.channels TO current_user; + +-- Index: public.channels_id_idx + +-- DROP INDEX public.channels_id_idx; + +CREATE INDEX IF NOT EXISTS channels_id_idx + ON public.channels + USING btree + (id COLLATE pg_catalog."default"); + diff --git a/invidious/config/sql/nonces.sql b/invidious/config/sql/nonces.sql new file mode 100644 index 0000000..644ac32 --- /dev/null +++ b/invidious/config/sql/nonces.sql @@ -0,0 +1,22 @@ +-- Table: public.nonces + +-- DROP TABLE public.nonces; + +CREATE TABLE IF NOT EXISTS public.nonces +( + nonce text, + expire timestamp with time zone, + CONSTRAINT nonces_id_key UNIQUE (nonce) +); + +GRANT ALL ON TABLE public.nonces TO current_user; + +-- Index: public.nonces_nonce_idx + +-- DROP INDEX public.nonces_nonce_idx; + +CREATE INDEX IF NOT EXISTS nonces_nonce_idx + ON public.nonces + USING btree + (nonce COLLATE pg_catalog."default"); + diff --git a/invidious/config/sql/playlist_videos.sql b/invidious/config/sql/playlist_videos.sql new file mode 100644 index 0000000..4b48b46 --- /dev/null +++ b/invidious/config/sql/playlist_videos.sql @@ -0,0 +1,19 @@ +-- Table: public.playlist_videos + +-- DROP TABLE public.playlist_videos; + +CREATE TABLE IF NOT EXISTS public.playlist_videos +( + title text, + id text, + author text, + ucid text, + length_seconds integer, + published timestamptz, + plid text references playlists(id), + index int8, + live_now boolean, + PRIMARY KEY (index,plid) +); + +GRANT ALL ON TABLE public.playlist_videos TO current_user; diff --git a/invidious/config/sql/playlists.sql b/invidious/config/sql/playlists.sql new file mode 100644 index 0000000..83efce4 --- /dev/null +++ b/invidious/config/sql/playlists.sql @@ -0,0 +1,29 @@ +-- Type: public.privacy + +-- DROP TYPE public.privacy; + +CREATE TYPE public.privacy AS ENUM +( + 'Public', + 'Unlisted', + 'Private' +); + +-- Table: public.playlists + +-- DROP TABLE public.playlists; + +CREATE TABLE IF NOT EXISTS public.playlists +( + title text, + id text primary key, + author text, + description text, + video_count integer, + created timestamptz, + updated timestamptz, + privacy privacy, + index int8[] +); + +GRANT ALL ON public.playlists TO current_user; diff --git a/invidious/config/sql/session_ids.sql b/invidious/config/sql/session_ids.sql new file mode 100644 index 0000000..c493769 --- /dev/null +++ b/invidious/config/sql/session_ids.sql @@ -0,0 +1,23 @@ +-- Table: public.session_ids + +-- DROP TABLE public.session_ids; + +CREATE TABLE IF NOT EXISTS public.session_ids +( + id text NOT NULL, + email text, + issued timestamp with time zone, + CONSTRAINT session_ids_pkey PRIMARY KEY (id) +); + +GRANT ALL ON TABLE public.session_ids TO current_user; + +-- Index: public.session_ids_id_idx + +-- DROP INDEX public.session_ids_id_idx; + +CREATE INDEX IF NOT EXISTS session_ids_id_idx + ON public.session_ids + USING btree + (id COLLATE pg_catalog."default"); + diff --git a/invidious/config/sql/users.sql b/invidious/config/sql/users.sql new file mode 100644 index 0000000..ad002ec --- /dev/null +++ b/invidious/config/sql/users.sql @@ -0,0 +1,29 @@ +-- Table: public.users + +-- DROP TABLE public.users; + +CREATE TABLE IF NOT EXISTS public.users +( + updated timestamp with time zone, + notifications text[], + subscriptions text[], + email text NOT NULL, + preferences text, + password text, + token text, + watched text[], + feed_needs_update boolean, + CONSTRAINT users_email_key UNIQUE (email) +); + +GRANT ALL ON TABLE public.users TO current_user; + +-- Index: public.email_unique_idx + +-- DROP INDEX public.email_unique_idx; + +CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx + ON public.users + USING btree + (lower(email) COLLATE pg_catalog."default"); + diff --git a/invidious/config/sql/videos.sql b/invidious/config/sql/videos.sql new file mode 100644 index 0000000..55da396 --- /dev/null +++ b/invidious/config/sql/videos.sql @@ -0,0 +1,23 @@ +-- Table: public.videos + +-- DROP TABLE public.videos; + +CREATE UNLOGGED TABLE IF NOT EXISTS public.videos +( + id text NOT NULL, + info text, + updated timestamp with time zone, + CONSTRAINT videos_pkey PRIMARY KEY (id) +); + +GRANT ALL ON TABLE public.videos TO current_user; + +-- Index: public.id_idx + +-- DROP INDEX public.id_idx; + +CREATE UNIQUE INDEX IF NOT EXISTS id_idx + ON public.videos + USING btree + (id COLLATE pg_catalog."default"); + diff --git a/invidious/docker-compose.yml b/invidious/docker-compose.yml new file mode 100644 index 0000000..c261a38 --- /dev/null +++ b/invidious/docker-compose.yml @@ -0,0 +1,81 @@ +services: + invidious: + image: quay.io/invidious/invidious:latest + restart: unless-stopped + # Remove "127.0.0.1:" if used from an external IP + ports: + - "3000:3000" + environment: + # Please read the following file for a comprehensive list of all available + # configuration options and their associated syntax: + # https://github.com/iv-org/invidious/blob/master/config/config.example.yml + INVIDIOUS_CONFIG: | + db: + dbname: invidious + user: kemal + password: ${POSTGRES_PASSWORD:-kemal} + host: invidious-db + port: 5432 + check_tables: true + invidious_companion: + - private_url: "http://companion:8282/companion" + # Use the key generated in the 2nd step + invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}" + # Use the key generated in the 1st step + hmac_key: "${INVIDIOUS_HMAC_KEY}" + healthcheck: + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1 + interval: 30s + timeout: 5s + retries: 2 + logging: + options: + max-size: "1G" + max-file: "4" + depends_on: + - invidious-db + + companion: + image: quay.io/invidious/invidious-companion:latest + # Please read the following file for a comprehensive list of all available + # environment variables and their associated syntax: + # https://github.com/iv-org/invidious/blob/master/config/config.example.yml + environment: + # Use the key generated in the 2nd step + - SERVER_SECRET_KEY=${INVIDIOUS_COMPANION_KEY} + restart: unless-stopped + # Uncomment only if you have configured "public_url" for Invidious companion + # Or if you want to use Invidious companion as an API in your program. + # Remove "127.0.0.1:" if used from an external IP + #ports: + # - "127.0.0.1:8282:8282" + logging: + options: + max-size: "1G" + max-file: "4" + cap_drop: + - ALL + read_only: true + # cache for youtube library + volumes: + - companioncache:/var/tmp/youtubei.js:rw + security_opt: + - no-new-privileges:true + + invidious-db: + image: docker.io/library/postgres:14 + restart: unless-stopped + volumes: + - postgresdata:/var/lib/postgresql/data + - ./config/sql:/config/sql + - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh + environment: + POSTGRES_DB: invidious + POSTGRES_USER: kemal + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-kemal} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + +volumes: + postgresdata: + companioncache: diff --git a/invidious/docker/Dockerfile b/invidious/docker/Dockerfile new file mode 100644 index 0000000..f837dae --- /dev/null +++ b/invidious/docker/Dockerfile @@ -0,0 +1,81 @@ +# https://github.com/openssl/openssl/releases/tag/openssl-3.6.2 +ARG OPENSSL_VERSION='3.6.2' +ARG OPENSSL_SHA256='aaf51a1fe064384f811daeaeb4ec4dce7340ec8bd893027eee676af31e83a04f' + +FROM 84codes/crystal:1.20.2-alpine AS dependabot-crystal + +# We compile openssl ourselves due to a memory leak in how crystal interacts +# with openssl +# Reference: https://github.com/iv-org/invidious/issues/1438#issuecomment-3087636228 +FROM dependabot-crystal AS openssl-builder +RUN apk add --no-cache curl perl linux-headers + +WORKDIR / + +ARG OPENSSL_VERSION +ARG OPENSSL_SHA256 +RUN curl -Ls "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" --output openssl-${OPENSSL_VERSION}.tar.gz +RUN echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c +RUN tar -xzvf openssl-${OPENSSL_VERSION}.tar.gz + +RUN cd openssl-${OPENSSL_VERSION} && ./Configure --openssldir=/etc/ssl && make -j$(nproc) + +FROM dependabot-crystal AS builder + +RUN apk add --no-cache sqlite-static yaml-static +RUN apk del openssl-dev openssl-libs-static + +ARG release + +WORKDIR /invidious +COPY ./shard.yml ./shard.yml +COPY ./shard.lock ./shard.lock +RUN shards install --production + +COPY ./src/ ./src/ +# TODO: .git folder is required for building – this is destructive. +# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. +COPY ./.git/ ./.git/ + +# Required for fetching player dependencies +COPY ./scripts/ ./scripts/ +COPY ./assets/ ./assets/ +COPY ./videojs-dependencies.yml ./videojs-dependencies.yml + +RUN crystal spec --warnings all \ + --link-flags "-lxml2 -llzma" + +ARG OPENSSL_VERSION +COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION} + +RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \ + PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \ + crystal build ./src/invidious.cr \ + --release \ + --static --warnings all \ + --link-flags "-lxml2 -llzma"; \ + else \ + PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \ + crystal build ./src/invidious.cr \ + --static --warnings all \ + --link-flags "-lxml2 -llzma"; \ + fi + +FROM alpine:3.23 +RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata +WORKDIR /invidious +RUN addgroup -g 1000 -S invidious && \ + adduser -u 1000 -S invidious -G invidious +COPY --chown=invidious ./config/config.* ./config/ +RUN mv -n config/config.example.yml config/config.yml +RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: invidious-db/' config/config.yml +COPY ./config/sql/ ./config/sql/ +COPY ./locales/ ./locales/ +COPY --from=builder /invidious/assets ./assets/ +COPY --from=builder /invidious/invidious . +RUN chmod o+rX -R ./assets ./config ./locales + +EXPOSE 3000 +USER invidious +ENTRYPOINT ["/sbin/tini", "--"] +CMD [ "/invidious/invidious" ] diff --git a/invidious/docker/init-invidious-db.sh b/invidious/docker/init-invidious-db.sh new file mode 100755 index 0000000..22b4cc5 --- /dev/null +++ b/invidious/docker/init-invidious-db.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -eou pipefail + +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql diff --git a/jellyfin/.env.example b/jellyfin/.env.example new file mode 100644 index 0000000..35c83a9 --- /dev/null +++ b/jellyfin/.env.example @@ -0,0 +1,2 @@ +SONARR_API_KEY=your_sonarr_api_key_here +RADARR_API_KEY=your_radarr_api_key_here diff --git a/jellyfin/README.md b/jellyfin/README.md new file mode 100644 index 0000000..7f64c6d --- /dev/null +++ b/jellyfin/README.md @@ -0,0 +1,59 @@ +# Jellyfin + *arr Stack + +Media server stack consisting of: +- **Jellyfin** — Media server (movies, TV, music, books) +- **Sonarr** — TV show management +- **Radarr** — Movie management +- **Prowlarr** — Indexer management +- **FlareSolverr** — Cloudflare bypass for indexers +- **Unpackerr** — Auto-extract downloaded archives +- **Recyclarr** — Automatic TRaSH-Guide sync for Sonarr/Radarr quality profiles + +## Setup + +### 1. Create the media user + +Run the setup script to create a dedicated `media` user (UID 1001) and set correct permissions: +``` +bash setup-media-user.sh +``` + +This script: +- Creates a `media` user/group with UID/GID 1001 +- Fixes ownership on config directories +- Sets group permissions on `/srv/` media directories + +### 2. Configure environment + +Copy `.env.example` to `.env` and set your API keys: +``` +cp .env.example .env +``` + +Get your API keys from Sonarr (Settings > General) and Radarr (Settings > General). + +### 3. Start + +``` +docker compose up -d +``` + +## Access + +| Service | URL | +|-------------|--------------------------| +| Jellyfin | `http://<host>:5000` | +| Sonarr | `http://<host>:8989` | +| Radarr | `http://<host>:7878` | +| Prowlarr | `http://<host>:9696` | +| FlareSolverr | `http://<host>:8191` | + +## What to Configure + +- **User/group IDs** — All services run as UID 1001/GID 1001. Change `user:`, `PUID`, and `PGID` throughout if your media user differs. +- **Time zone** — Replace `TZ=America/Los_Angeles` throughout with your timezone. +- **Media volumes** — All services mount `/srv:/data`. Your media root should be `/srv/` with subdirectories like `vids/`, `mus/`, etc. Adjust the host path if your storage layout differs. +- **Fonts** — Jellyfin mounts `/usr/local/share/fonts/` for custom fonts. Remove if not needed. +- **Hardware transcoding** — Jellyfin has `/dev/dri` passed through for VAAPI. Remove or adjust for your GPU setup. +- **API keys** — Set `SONARR_API_KEY` and `RADARR_API_KEY` in `.env` for Unpackerr. You also need to configure these keys inside Unpackerr's web UI on first run. +- **Recyclarr** — Configure `recyclarr-config/recyclarr.yml` to sync TRaSH-Guide profiles. Runs daily at 4 AM. diff --git a/jellyfin/docker-compose.yml b/jellyfin/docker-compose.yml new file mode 100644 index 0000000..23785b7 --- /dev/null +++ b/jellyfin/docker-compose.yml @@ -0,0 +1,133 @@ +services: + jellyfin: + image: jellyfin/jellyfin + container_name: jellyfin + user: 1001:1001 + ports: + - 5000:8096/tcp + - 7359:7359/udp + group_add: + - "44" # video (for /dev/dri/card0) + - "992" # render (for /dev/dri/renderD128) + devices: + - /dev/dri:/dev/dri + volumes: + - ./config:/config + - ./cache:/cache + - type: bind + source: /srv/ + target: /media + read_only: true + - type: bind + source: /usr/local/share/fonts/ + target: /usr/local/share/fonts/custom + read_only: true + restart: unless-stopped + environment: + - LIBVA_DRIVER_NAME=i965 + extra_hosts: + - 'host.docker.internal:host-gateway' + + sonarr: + image: linuxserver/sonarr + container_name: sonarr + environment: + - PUID=1001 + - PGID=1001 + - TZ=America/Los_Angeles # Change this to your timezone, e.g. America/Los_Angeles + - UMASK=002 + ports: + - 8989:8989 + volumes: + - ./sonarr-config:/config + - /srv:/data + restart: unless-stopped + + radarr: + image: linuxserver/radarr + container_name: radarr + environment: + - PUID=1001 + - PGID=1001 + - TZ=America/Los_Angeles # Change this to your timezone, e.g. America/Los_Angeles + - UMASK=002 + ports: + - 7878:7878 + volumes: + - ./radarr-config:/config + - /srv:/data + restart: unless-stopped + + prowlarr: + image: linuxserver/prowlarr + container_name: prowlarr + environment: + - PUID=1001 + - PGID=1001 + - TZ=America/Los_Angeles + - UMASK=002 + ports: + - 9696:9696 + volumes: + - ./prowlarr-config:/config + restart: unless-stopped + + flaresolverr: + image: ghcr.io/flaresolverr/flaresolverr:latest + container_name: flaresolverr + environment: + - LOG_LEVEL=info + - TZ=America/Los_Angeles + ports: + - 8191:8191 + restart: unless-stopped + + unpackerr: + image: golift/unpackerr + container_name: unpackerr + user: 1000:1001 + env_file: + - .env + environment: + - TZ=America/Los_Angeles + - UN_SONARR_0_URL=http://sonarr:8989 + - UN_SONARR_0_API_KEY=${SONARR_API_KEY} + - UN_SONARR_0_PATHS_0=/data/torrents/downloads + - UN_SONARR_0_PROTOCOLS=torrent + - UN_RADARR_0_URL=http://radarr:7878 + - UN_RADARR_0_API_KEY=${RADARR_API_KEY} + - UN_RADARR_0_PATHS_0=/data/torrents/downloads + - UN_RADARR_0_PROTOCOLS=torrent + - UN_CMDHOOK_0_COMMAND=chmod -R g+rw "{{ .Path }}" + - UN_CMDHOOK_0_EXTRACTED=true + - UN_CMDHOOK_0_SHELL=true + volumes: + - /srv:/data + restart: unless-stopped + + recyclarr: + image: ghcr.io/recyclarr/recyclarr + container_name: recyclarr + user: 1001:1001 + environment: + - TZ=America/Los_Angeles + - CRON_SCHEDULE=0 4 * * * + volumes: + - ./recyclarr-config:/config + restart: unless-stopped + + # If you add a download client later, mount it the same way: + # qbittorrent: + # image: linuxserver/qbittorrent + # container_name: qbittorrent + # environment: + # - PUID=1001 + # - PGID=1001 + # - TZ=America/Los_Angeles + # - UMASK=002 + # ports: + # - 8080:8080 + # volumes: + # - ./qbittorrent-config:/config + # - /srv:/data + # restart: unless-stopped diff --git a/jellyfin/setup-media-user.sh b/jellyfin/setup-media-user.sh new file mode 100755 index 0000000..d470c16 --- /dev/null +++ b/jellyfin/setup-media-user.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e + +echo "=== Creating dedicated media user (UID 1001) and group (GID 1001) ===" + +# Create the media group +sudo groupadd -g 1001 media + +# Create the media user (no home dir, no login shell) +sudo useradd -u 1001 -g media -s /usr/sbin/nologin -M media + +# Add tyler to media group so tyler can access files created by the media user +sudo usermod -aG media tyler + +# Add media user to video and render groups for hardware transcoding access +sudo usermod -aG video,render media + +echo "=== Fixing Jellyfin config/cache ownership ===" +sudo chown -R 1001:1001 /home/tyler/docker/jellyfin/config +sudo chown -R 1001:1001 /home/tyler/docker/jellyfin/cache + +echo "=== Creating config directories for Sonarr and Radarr ===" +sudo mkdir -p /home/tyler/docker/jellyfin/sonarr-config +sudo mkdir -p /home/tyler/docker/jellyfin/radarr-config +sudo chown -R 1001:1001 /home/tyler/docker/jellyfin/sonarr-config +sudo chown -R 1001:1001 /home/tyler/docker/jellyfin/radarr-config + +echo "=== Setting media group ownership on /srv/ directories ===" +# Change group to media on all media directories +sudo chown -R :media /srv/vids /srv/mus /srv/books /srv/photos /srv/roms /srv/torrents + +echo "=== Clearing conflicting POSIX ACLs (if any) ===" +# ACLs from NAS/Samba can override Unix group permissions. +# Strip them so our chmod/chown rules actually take effect. +if command -v setfacl &>/dev/null; then + sudo setfacl -R -b /srv/vids /srv/mus /srv/books /srv/photos /srv/roms /srv/torrents 2>/dev/null || true +else + echo "Warning: setfacl not found. Install the 'acl' package if you have ACL issues." +fi + +echo "=== Setting directory sticky bit (g+s) so new files inherit media group ===" +sudo find /srv/vids /srv/mus /srv/books /srv/photos /srv/roms /srv/torrents -type d -exec chmod g+s {} + + +echo "=== Setting base permissions ===" +# Group read+execute on all directories +sudo find /srv/vids /srv/mus /srv/books /srv/photos /srv/roms /srv/torrents -type d -exec chmod g+rx {} + + +# Group read on all files +sudo find /srv/vids /srv/mus /srv/books /srv/photos /srv/roms /srv/torrents -type f -exec chmod g+r {} + + +echo "=== Setting write permissions for Sonarr/Radarr managed directories ===" +# Allow group write on top-level dirs and recursively within them +sudo chmod g+w /srv/vids/movies /srv/vids/shows /srv/torrents +sudo find /srv/vids/movies /srv/vids/shows /srv/torrents -type d -exec chmod g+w {} + +sudo find /srv/vids/movies /srv/vids/shows /srv/torrents -type f -exec chmod g+w {} + + +echo "=== Done! ===" +echo "" +echo "You will need to log out and back in (or run 'newgrp media') for the" +echo "group changes to take effect in your current shell." +echo "" +echo "Start the stack with: docker compose up -d" +echo "" +echo "=== Web UI Configuration ===" +echo "Sonarr (http://<host>:8989):" +echo " - Root folder: /data/vids/shows" +echo " - Download client path: /data/torrents" +echo "" +echo "Radarr (http://<host>:7878):" +echo " - Root folder: /data/vids/movies" +echo " - Download client path: /data/torrents" +echo "" +echo "If you add a download client later, mount it with /srv:/data too" +echo "and set its download directory to /data/torrents." +echo "This ensures paths match and hard links / fast moves work correctly." diff --git a/openwebui/.env.example b/openwebui/.env.example new file mode 100644 index 0000000..7f95b8f --- /dev/null +++ b/openwebui/.env.example @@ -0,0 +1 @@ +OPEN_TERMINAL_API_KEY=your_api_key_here diff --git a/openwebui/README.md b/openwebui/README.md new file mode 100644 index 0000000..dd8de37 --- /dev/null +++ b/openwebui/README.md @@ -0,0 +1,35 @@ +# Open WebUI + +Self-hosted web interface for interacting with LLMs (Ollama, OpenAI-compatible APIs, etc.). + +Includes: +- **Open WebUI** — Main chat interface +- **Open Terminal** — Browser-based terminal +- **Apache Tika** — Document parsing for RAG + +## Setup + +1. Copy `.env.example` to `.env` and set your API key: + ``` + cp .env.example .env + ``` + +2. Start: + ``` + docker compose up -d + ``` + +## Access + +| Service | URL | +|---------------|--------------------------| +| Open WebUI | `http://<host>:3000` | +| Open Terminal | `http://<host>:8001` | +| Tika | `http://<host>:9998` | + +## What to Configure + +- **`.env`** — Set `OPEN_TERMINAL_API_KEY` to a secure random string. +- **Ollama / LLM backend** — The `host.docker.internal` extra host is configured to reach a local Ollama instance. If your LLM backend runs elsewhere, update the host or add environment variables like `OPENAI_API_BASE_URL`. +- **RAG settings** — `RAG_EMBEDDING_ENGINE=openai` uses OpenAI-compatible embeddings. Change if using a different engine. +- **Persistent volumes** — `open-webui` and `open-terminal` are named Docker volumes. Data persists across container restarts. diff --git a/openwebui/docker-compose.yml b/openwebui/docker-compose.yml new file mode 100644 index 0000000..ff2832b --- /dev/null +++ b/openwebui/docker-compose.yml @@ -0,0 +1,38 @@ +services: + openwebui: + image: ghcr.io/open-webui/open-webui:main + ports: + - "3000:8080" + volumes: + - open-webui:/app/backend/data + environment: + - ENABLE_KB_EXEC=true + - MODELS_CACHE_TTL=300 + - ENABLE_REALTIME_CHAT_SAVE=False + - RAG_EMBEDDING_ENGINE=openai + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + + open-terminal: + image: ghcr.io/open-webui/open-terminal + container_name: open-terminal + ports: + - "8001:8000" + volumes: + - open-terminal:/home/user + environment: + - OPEN_TERMINAL_MULTI_USER=true + - OPEN_TERMINAL_API_KEY=${OPEN_TERMINAL_API_KEY} + restart: unless-stopped + tika: + image: apache/tika:latest-full + container_name: tika + ports: + - "9998:9998" + restart: unless-stopped + + +volumes: + open-webui: + open-terminal: diff --git a/watchtower/.env.example b/watchtower/.env.example new file mode 100644 index 0000000..a5e5686 --- /dev/null +++ b/watchtower/.env.example @@ -0,0 +1,6 @@ +SMTP_FROM=watchtower@example.com +SMTP_TO=you@example.com +SMTP_SERVER=mail.example.com +SMTP_PORT=587 +SMTP_USER=you@example.com +SMTP_PASS=your_smtp_password_here diff --git a/watchtower/README.md b/watchtower/README.md new file mode 100644 index 0000000..1d3bc8b --- /dev/null +++ b/watchtower/README.md @@ -0,0 +1,25 @@ +# Watchtower + +Automatically updates running Docker containers to the latest image versions. + +## Setup + +1. Copy `.env.example` to `.env`: + ``` + cp .env.example .env + ``` + +2. Edit `.env` with your SMTP credentials for email notifications. + +3. Start: + ``` + docker compose up -d + ``` + +## What to Configure + +- **`.env`** — All SMTP settings. Watchtower will send email notifications after updates (or if updates fail). +- **Schedule** — `WATCHTOWER_SCHEDULE=0 0 4 * * 0` runs every Sunday at 4 AM. Uses 6-field cron. Change as desired. +- **Docker socket** — Mounts `/var/run/docker.sock`. Required for Watchtower to manage containers. +- **Cleanup** — `WATCHTOWER_CLEANUP=true` removes old images after updates. Set to `false` if you want to keep them. +- **Scope labels** — To exclude a container from auto-updates, add `com.centurylinklabs.watchtower.scope=ignore` to its container labels. diff --git a/watchtower/docker-compose.yml b/watchtower/docker-compose.yml new file mode 100644 index 0000000..73a1c60 --- /dev/null +++ b/watchtower/docker-compose.yml @@ -0,0 +1,19 @@ +services: + watchtower: + image: nickfedor/watchtower + container_name: watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WATCHTOWER_SCHEDULE=0 0 4 * * 0 # Sundays 4am (6-field cron) + - WATCHTOWER_CLEANUP=true # remove old images after update + - WATCHTOWER_INCLUDE_RESTARTING=true + - WATCHTOWER_NOTIFICATIONS=email + - WATCHTOWER_NOTIFICATION_EMAIL_FROM=${SMTP_FROM} + - WATCHTOWER_NOTIFICATION_EMAIL_TO=${SMTP_TO} + - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=${SMTP_SERVER} + - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=${SMTP_PORT} + - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=${SMTP_USER} + - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=${SMTP_PASS} + - WATCHTOWER_NOTIFICATION_EMAIL_USE_SSL_SMTP=true # for STARTTLS on 587 |
