diff --git a/.gitignore b/.gitignore index b1b151e..def64d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea secrets.yml venv +stack.env diff --git a/ansible/library/portainer.py b/ansible/library/portainer.py index f3d79dd..a5bf2c6 100755 --- a/ansible/library/portainer.py +++ b/ansible/library/portainer.py @@ -105,6 +105,20 @@ def _query_params_to_string(params): return s +def _load_envs_from_file(filepath): + envs = [] + with open(filepath) as f: + file_contents = f.read() + lines = file_contents.splitlines() + for line in lines: + name, value = line.split("=") + envs.append({ + "name": name, + "value": value + }) + return envs + + class PortainerClient: def __init__(self, creds): self.base_url = creds["base_url"] @@ -146,10 +160,12 @@ class PortainerClient: return res.json() -def _create_stack(client, module, file_contents): +def _create_stack(client, module, file_contents, envs=None): + if not envs: + envs = [] target_stack_name = module.params["stack_name"] body = { - "env": [], + "env": envs, "name": target_stack_name, "stackFileContent": file_contents, } @@ -162,12 +178,14 @@ def _create_stack(client, module, file_contents): return client.post("stacks", body=body, query_params=query_params) -def _update_stack(client, module, stack_id): +def _update_stack(client, module, stack_id, envs=None): + if not envs: + envs = [] target_stack_name = module.params["stack_name"] with open(module.params["docker_compose_file_path"]) as f: file_contents = f.read() return client.put(f"stacks/{stack_id}?&endpointId=2", body={ - "env": [], + "env": envs, "name": target_stack_name, "stackFileContent": file_contents, }) @@ -186,6 +204,10 @@ def handle_state_present(client, module): with open(module.params["docker_compose_file_path"]) as f: file_contents = f.read() + envs = [] + if "env_file_path" in module.params: + envs = _load_envs_from_file(module.params["env_file_path"]) + target_stack_name = module.params["stack_name"] for stack in stacks: if stack["Name"] == target_stack_name: @@ -194,7 +216,7 @@ def handle_state_present(client, module): break if not already_exists: - stack = _create_stack(client, module, file_contents) + stack = _create_stack(client, module, file_contents, envs=envs) result["changed"] = True result["stack_id"] = stack["Id"] module.exit_json(**result) @@ -211,7 +233,7 @@ def handle_state_present(client, module): return # the stack exists and we have a new config. - _update_stack(client, module, stack_id) + _update_stack(client, module, stack_id, envs=envs) result["changed"] = True module.exit_json(**result) diff --git a/ansible/roles/setup_hosted_services/defaults/main.yml b/ansible/roles/setup_hosted_services/defaults/main.yml index 9175357..11f344f 100644 --- a/ansible/roles/setup_hosted_services/defaults/main.yml +++ b/ansible/roles/setup_hosted_services/defaults/main.yml @@ -19,6 +19,14 @@ services: volumes: ["plex_config", "plex_tautulli_config"] - name: uptime-kuma volumes: ["uptime-kuma_data"] + - name: vpn-stack + volumes: ["vpn-stack_qbittorrent_config", "vpn-stack_radarr_config", "vpn-stack_sonarr_config", "vpn-stack_jackett_config"] + - name: docker-volume-backup + volumes: [] + - name: mariadb + volumes: ["mariadb_config","mariadb_data"] + - name: photoprism + volumes: [] docker_networks: - nextcloud_net diff --git a/ansible/roles/setup_hosted_services/files/docker-volume-backup/docker-compose.yml b/ansible/roles/setup_hosted_services/files/docker-volume-backup/docker-compose.yml new file mode 100644 index 0000000..5f6d91e --- /dev/null +++ b/ansible/roles/setup_hosted_services/files/docker-volume-backup/docker-compose.yml @@ -0,0 +1,29 @@ +# https://app.idrivee2.com/region/IE/buckets/backups/object-storage +version: "3" +services: + docker-volume-backup: + container_name: docker-volume-backup + restart: always + image: ghcr.io/chatton/docker-volume-backup:v0.3.0 + command: + - periodic-backups + - --cron + - "0 3 * * *" + - --host-path + - /mnt/hdds/backups/ + - --retention-days + - "7" + - --modes + - "filesystem,s3" + environment: + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION} + AWS_BUCKET: ${AWS_BUCKET} + AWS_ENDPOINT: ${AWS_ENDPOINT} + + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /mnt/hdds/backups:/backups + - /tmp:/tmp + diff --git a/ansible/roles/setup_hosted_services/files/gitea/docker-compose.yml b/ansible/roles/setup_hosted_services/files/gitea/docker-compose.yml index 89e16c0..d098fef 100644 --- a/ansible/roles/setup_hosted_services/files/gitea/docker-compose.yml +++ b/ansible/roles/setup_hosted_services/files/gitea/docker-compose.yml @@ -18,4 +18,4 @@ services: - "3000:3000" - "222:22" volumes: - data: \ No newline at end of file + data: diff --git a/ansible/roles/setup_hosted_services/files/mariadb/docker-compose.yml b/ansible/roles/setup_hosted_services/files/mariadb/docker-compose.yml new file mode 100644 index 0000000..f89e814 --- /dev/null +++ b/ansible/roles/setup_hosted_services/files/mariadb/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.1' +services: + mariadb: + labels: + ie.cianhatton.backup.enabled: "true" + container_name: mariadb + image: mariadb + hostname: mariadb + restart: unless-stopped + ports: + - 3306:3306 + volumes: + - data:/var/lib/mysql + - config:/etc/mysql/conf.d + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + + adminer: + restart: unless-stopped + image: adminer:latest + environment: + ADMINER_DEFAULT_SERVER: mariadb + ADMINER_DESIGN: galkaev + ports: + - 3307:8080 +volumes: + data: + config: + + +networks: + default: + name: nextcloud_net + external: true \ No newline at end of file diff --git a/ansible/roles/setup_hosted_services/files/photoprism/docker-compose.yml b/ansible/roles/setup_hosted_services/files/photoprism/docker-compose.yml new file mode 100644 index 0000000..357f5a3 --- /dev/null +++ b/ansible/roles/setup_hosted_services/files/photoprism/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.5' +services: + photoprism: + image: photoprism/photoprism:latest + container_name: photoprism + restart: unless-stopped + security_opt: + - seccomp:unconfined + - apparmor:unconfined + ports: + - "2342:2342" # HTTP port (host:container) + environment: + PHOTOPRISM_ADMIN_PASSWORD: ${PHOTOPRISM_ADMIN_PASSWORD} # INITIAL PASSWORD FOR "admin" USER, MINIMUM 8 CHARACTERS + PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password) + PHOTOPRISM_SITE_URL: "http://localhost:2342/" # public server URL incl http:// or https:// and /path, :port is optional + PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video) + PHOTOPRISM_HTTP_COMPRESSION: "gzip" # improves transfer speed and bandwidth utilization (none or gzip) + PHOTOPRISM_LOG_LEVEL: "info" # log level: trace, debug, info, warning, error, fatal, or panic + PHOTOPRISM_READONLY: "false" # do not modify originals directory (reduced functionality) + PHOTOPRISM_EXPERIMENTAL: "false" # enables experimental features + PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup + PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server + PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API + PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow + PHOTOPRISM_DISABLE_FACES: "false" # disables face detection and recognition (requires TensorFlow) + PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification (requires TensorFlow) + PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files + PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance) + PHOTOPRISM_JPEG_QUALITY: 85 # a higher value increases the quality and file size of JPEG images and thumbnails (25-100) + PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow) + PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive (no effect without TensorFlow) + PHOTOPRISM_DATABASE_DRIVER: "mysql" # use MariaDB 10.5+ or MySQL 8+ instead of SQLite for improved performance + PHOTOPRISM_DATABASE_SERVER: "mariadb:3306" # MariaDB or MySQL database server (hostname:port) + PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name + PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name + PHOTOPRISM_DATABASE_PASSWORD: ${PHOTOPRISM_DATABASE_PASSWORD} # MariaDB or MySQL database user password + PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App" + PHOTOPRISM_SITE_DESCRIPTION: "" # meta site description + PHOTOPRISM_SITE_AUTHOR: "" # meta site author + ## Share hardware devices with FFmpeg and TensorFlow (optional): + devices: + - "/dev/dri:/dev/dri" # Intel QSV + working_dir: "/photoprism" # do not change or remove + volumes: + - "/mnt/hdds/photoprism/originals:/photoprism/originals" # Original media files (DO NOT REMOVE) + - "/mnt/hdds/photoprism/import:/photoprism/import" # *Optional* base folder from which files can be imported to originals + - "/mnt/hdds/photoprism/storage:/photoprism/storage" # *Writable* storage folder for cache, database, and sidecar files (DO NOT REMOVE) + + +networks: + default: + name: nextcloud_net + external: true diff --git a/ansible/roles/setup_hosted_services/files/vpn-stack/docker-compose.yml b/ansible/roles/setup_hosted_services/files/vpn-stack/docker-compose.yml new file mode 100644 index 0000000..708fe83 --- /dev/null +++ b/ansible/roles/setup_hosted_services/files/vpn-stack/docker-compose.yml @@ -0,0 +1,110 @@ +version: "3" +services: + surfshark: + image: ilteoood/docker-surfshark + container_name: surfshark + environment: + - SURFSHARK_USER=${SURFSHARK_USER} + - SURFSHARK_PASSWORD=${SURFSHARK_PASSWORD} + # must specify LAN_NETWORK otherwise you will not be able + # to access ports which are exposed here. + - LAN_NETWORK=${LAN_NETWORK} + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun + ports: + # qbittorrent + - 15000:15000 + - 6881:6881 + - 6881:6881/udp + # radarr + - 7878:7878 + # sonarr + - 8989:8989 + # jackett + - 9117:9117 + + restart: unless-stopped + dns: + - 1.1.1.1 + - 8.8.8.8 + + qbittorrent: + labels: + ie.cianhatton.backup.enabled: "true" + + depends_on: + - surfshark + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + network_mode: service:surfshark + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + - WEBUI_PORT=15000 + volumes: + - "qbittorrent_config:/config" + - "/mnt/ssd0/downloads:/downloads" + restart: unless-stopped + + radarr: + labels: + ie.cianhatton.backup.enabled: "true" + depends_on: + - surfshark + image: lscr.io/linuxserver/radarr:latest + container_name: radarr + network_mode: service:surfshark + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + volumes: + - "radarr_config:/config" + - "/mnt/hdds/media/movies:/movies" + - "/mnt/ssd0/downloads:/downloads" + restart: unless-stopped + + sonarr: + depends_on: + - surfshark + image: lscr.io/linuxserver/sonarr:latest + labels: + ie.cianhatton.backup.enabled: "true" + container_name: sonarr + network_mode: service:surfshark + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + volumes: + - "sonarr_config:/config" + - "/mnt/hdds/media/tv:/tv" + - "/mnt/ssd0/downloads:/downloads" + restart: unless-stopped + + jackett: + labels: + ie.cianhatton.backup.enabled: "true" + depends_on: + - surfshark + image: lscr.io/linuxserver/jackett:latest + container_name: jackett + network_mode: service:surfshark + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + - AUTO_UPDATE=true + volumes: + - "jackett_config:/config" + - "/mnt/ssd0/downloads:/downloads" + restart: unless-stopped + +volumes: + qbittorrent_config: + radarr_config: + sonarr_config: + jackett_config: \ No newline at end of file diff --git a/ansible/roles/setup_hosted_services/tasks/main.yml b/ansible/roles/setup_hosted_services/tasks/main.yml index 0577c61..8a423fc 100644 --- a/ansible/roles/setup_hosted_services/tasks/main.yml +++ b/ansible/roles/setup_hosted_services/tasks/main.yml @@ -11,6 +11,12 @@ dest: "{{docker_compose_directory}}/{{item.name}}/docker-compose.yml" with_items: "{{services}}" +- name: Docker Compose | Copy Stack Env File + copy: + src: "{{item.name}}/stack.env" + dest: "{{docker_compose_directory}}/{{item.name}}/stack.env" + with_items: "{{services}}" + - name: Config Files | Create a directory if it does not exist file: path: "{{item.destination_directory}}" @@ -77,5 +83,6 @@ username: admin password: "{{portainer.password}}" docker_compose_file_path: "{{docker_compose_directory}}/{{ item.name }}/docker-compose.yml" + env_file_path: "{{docker_compose_directory}}/{{ item.name }}/stack.env" stack_name: "{{ item.name }}" with_items: "{{services}}"