chore: Refactor swarm setup

This commit is contained in:
Tony Du 2025-02-08 13:21:33 -08:00
parent 95ac0318ed
commit 2758ecf5d3
12 changed files with 607 additions and 254 deletions

View File

@ -0,0 +1 @@
secrets.yml

View File

@ -27,7 +27,7 @@ app_domain_name: stingray.mnke.org
# So I'm ending up back where I started :)
nfs_export_path: /mnt/emc14t9/managed/stingray
nfs_server: truenas.local
nfs_mount_path: /mnt/stingray/
nfs_mount_path: /mnt/stingray
portainer_app_name: portainer
portainer_admin_password: "{{ secrets.portainer_admin_password }}"
@ -36,8 +36,6 @@ portainer_agent_secret: "{{ secrets.portainer_agent_secret }}"
cf_dns_api_token: "{{ secrets.cf_dns_api_token }}"
cf_email: tonydu121@hotmail.com
traefik_listen_port: 80
traefik_secure_listen_port: 443
traefik_admin_port: 8080
traefik_admin_user: admin
traefik_admin_password: "{{ secrets.traefik_admin_password }}"

View File

@ -4,35 +4,10 @@
mount:
src: "{{nfs_server}}:{{nfs_export_path}}"
path: "{{nfs_mount_path}}"
opts: rw,sync,hard,intr
opts: rw,_netdev,hard,intr,nolock
state: mounted
fstype: nfs
- name: Verify stacks directory exists (on first swarm node)
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "/home/{{ansible_user}}/stacks/swarm-bootstrap"
state: directory
- name: Verify bootstrap volume path (on first swarm node)
become: true
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "{{ item }}"
state: directory
loop:
- "{{nfs_mount_path}}/swarm-bootstrap/traefik/letsencrypt"
- "{{nfs_mount_path}}/swarm-bootstrap/traefik/secrets"
- "{{nfs_mount_path}}/swarm-bootstrap/portainer"
- "{{nfs_mount_path}}/swarm-bootstrap/gitea"
- name: Create CF secret
become: true
copy:
content: "{{ cf_dns_api_token }}"
dest: "{{nfs_mount_path}}/swarm-bootstrap/traefik/secrets/cf-dns-api-token.secret"
mode: '0740'
- name: Set DNS servers
become: true
tags: [set_dns_servers]
@ -50,46 +25,7 @@
name: systemd-resolved
state: restarted
when: dns_servers_configuration.changed
retries: 2
- name: Generate Traefik admin password hash
when: inventory_hostname == groups['swarm_managers'][0]
shell: echo $(htpasswd -nb {{traefik_admin_user}} {{traefik_admin_password}}) | sed -e s/\\$/\\$\\$/g
register: traefikpassword
changed_when: false
- name: Generate Portainer admin password hash
when: inventory_hostname == groups['swarm_managers'][0]
shell: echo $(htpasswd -nBb admin {{portainer_admin_password}}) | cut -d ":" -f 2 | sed -e s/\\$/\\$\\$/g
register: portainerpassword
changed_when: false
- name: Create git user
become: true
user:
name: git
create_home: true
register: git_user
- set_fact:
portainer_htpasswd: "{{portainerpassword.stdout}}"
traefik_htpasswd: "{{traefikpassword.stdout}}"
git_user_id: "{{git_user.uid}}"
git_group_id: "{{git_user.group}}"
when: inventory_hostname == groups['swarm_managers'][0]
- name: Create docker-compose stack file (on first swarm node)
when: inventory_hostname == groups['swarm_managers'][0]
template:
src: docker-stack.yml.j2
dest: /home/{{ansible_user}}/stacks/swarm-bootstrap/docker-stack.yml
mode: 0755
- name: Deploy stack from a compose file (on first swarm node)
when: inventory_hostname == groups['swarm_managers'][0]
become: true
docker_stack:
state: present
name: swarm-bootstrap
detach: false
compose:
- /home/{{ansible_user}}/stacks/swarm-bootstrap/docker-stack.yml
- import_tasks: traefik.yml
- import_tasks: portainer.yml

View File

@ -0,0 +1,44 @@
- name: Verify stacks directory exists
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "/home/{{ansible_user}}/stacks/portainer"
state: directory
mode: 0755
- name: Verify volume paths
become: true
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "{{ item }}"
state: directory
mode: 0755
loop:
- "{{nfs_mount_path}}/portainer"
- "{{nfs_mount_path}}/portainer/data"
- name: Generate Portainer admin password hash
when: inventory_hostname == groups['swarm_managers'][0]
shell: echo $(htpasswd -nBb admin {{portainer_admin_password}}) | cut -d ":" -f 2 | sed -e s/\\$/\\$\\$/g
register: portainerpassword
changed_when: false
- set_fact:
portainer_htpasswd: "{{portainerpassword.stdout}}"
when: inventory_hostname == groups['swarm_managers'][0]
- name: Create Portainer docker-compose stack file
when: inventory_hostname == groups['swarm_managers'][0]
template:
src: portainer/docker-stack.yml.j2
dest: /home/{{ansible_user}}/stacks/portainer/docker-stack.yml
mode: 0755
- name: Deploy Portainer stack from a compose file
when: inventory_hostname == groups['swarm_managers'][0]
become: true
docker_stack:
state: present
name: portainer
detach: false
compose:
- /home/{{ansible_user}}/stacks/portainer/docker-stack.yml

View File

@ -0,0 +1,64 @@
---
- name: Verify stacks directory exists
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "/home/{{ansible_user}}/stacks/traefik"
state: directory
mode: 0755
- name: Verify volume paths
become: true
when: inventory_hostname == groups['swarm_managers'][0]
file:
path: "{{ item }}"
state: directory
mode: 0755
loop:
- "{{nfs_mount_path}}/traefik/letsencrypt"
- "{{nfs_mount_path}}/traefik/secrets"
- "{{nfs_mount_path}}/traefik/config"
- "{{nfs_mount_path}}/traefik/config/dynamic"
- name: Create CF secret
become: true
when: inventory_hostname == groups['swarm_managers'][0]
copy:
content: "{{ cf_dns_api_token }}"
dest: "{{nfs_mount_path}}/traefik/secrets/cf-dns-api-token.secret"
mode: '0740'
- name: Generate Traefik admin password hash
when: inventory_hostname == groups['swarm_managers'][0]
shell: echo $(htpasswd -nb {{traefik_admin_user}} {{traefik_admin_password}}) | sed -e s/\\$/\\$\\$/g
register: traefikpassword
changed_when: false
- set_fact:
traefik_htpasswd: "{{traefikpassword.stdout}}"
when: inventory_hostname == groups['swarm_managers'][0]
- name: Create Traefik docker-compose stack file
when: inventory_hostname == groups['swarm_managers'][0]
template:
src: traefik/docker-stack.yml.j2
dest: /home/{{ansible_user}}/stacks/traefik/docker-stack.yml
mode: 0755
- name: Template Traefik configuration
when: inventory_hostname == groups['swarm_managers'][0]
become: true
template:
src: traefik/config/traefik.yml.j2
dest: "{{nfs_mount_path}}/traefik/config/traefik.yml"
mode: 0755
- name: Deploy Traefik stack from a compose file
when: inventory_hostname == groups['swarm_managers'][0]
become: true
docker_stack:
state: present
name: traefik
detach: false
compose:
- /home/{{ansible_user}}/stacks/traefik/docker-stack.yml

View File

@ -1,183 +0,0 @@
networks:
gitea:
driver: overlay
attachable: true
name: gitea
traefik:
driver: overlay
attachable: true
name: traefik
portainer:
driver: overlay
attachable: true
name: portainer
volumes:
gitea:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/swarm-bootstrap/gitea
name: gitea
portainer_data:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/swarm-bootstrap/portainer
name: portainer_data
traefik:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/swarm-bootstrap/traefik
name: traefik
secrets:
cf_dns_api_token:
file: "{{nfs_mount_path}}/swarm-bootstrap/traefik/secrets/cf-dns-api-token.secret"
services:
traefik:
image: traefik:v3.3
dns:
- 1.1.1.1
command:
- "--log.level=DEBUG"
- "--api.dashboard=true"
# Allow invalid TLS certs internally
- "--api.insecure=true"
# Swarm settings
- "--providers.swarm=true"
- "--providers.swarm.exposedByDefault=false"
- "--providers.swarm.endpoint=unix:///var/run/docker.sock"
# HTTP
- "--entrypoints.web.address=:{{traefik_listen_port}}"
# Redirect to HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:{{traefik_secure_listen_port}}"
# TLS
- "--entrypoints.websecure.http.tls.domains[0].main=stingray.mnke.org"
- "--entrypoints.websecure.http.tls.domains[0].sans=*.stingray.mnke.org"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.letsencrypt.acme.caserver={{'https://acme-v02.api.letsencrypt.org/directory' if traefik_tls_mode == 'production' else 'https://acme-staging-v02.api.letsencrypt.org/directory'}}"
- "--certificatesresolvers.letsencrypt.acme.email={{cf_email}}"
- "--certificatesresolvers.letsencrypt.acme.storage=/data/letsencrypt/acme.json"
ports:
- "{{traefik_listen_port}}:{{traefik_listen_port}}"
- "{{traefik_secure_listen_port}}:{{traefik_secure_listen_port}}"
- "{{traefik_admin_port}}:8080"
secrets:
- "cf_dns_api_token"
environment:
- "CLOUDFLARE_EMAIL={{cf_email}}"
- "CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik:/data
networks:
- traefik
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.{{app_domain_name}}`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=auth"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.auth.basicauth.users={{traefik_htpasswd}}"
# Dummy service for Swarm port detection. The port can be any valid integer value.
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
mode: global
placement:
constraints: [node.role == manager]
whoami:
image: "traefik/whoami"
networks:
- traefik
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.stingray.mnke.org`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.swarm.network=traefik"
agent:
image: portainer/agent:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- portainer
environment:
AGENT_SECRET: {{portainer_agent_secret}}
deploy:
mode: global
placement:
constraints: [node.platform.os == linux]
portainer:
image: portainer/portainer:latest
command: "-H tcp://tasks.agent:9001 --tlsskipverify --bind :9000 --tunnel-port 8000 --admin-password {{portainer_htpasswd}}"
ports:
- "9000:9000"
- "8000:8000"
volumes:
- portainer_data:/data
networks:
- portainer
- traefik
environment:
# TODO: Load this in a secret
AGENT_SECRET: {{portainer_agent_secret}}
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.{{app_domain_name}}`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
gitea:
image: docker.io/gitea/gitea:1.23.1
environment:
- USER_UID={{git_user_id}}
- USER_GID={{git_group_id}}
- USER=git
- GITEA_APP_NAME=mnke
- GITEA__server__DOMAIN={{gitea_primary_domain_name}}
- GITEA__server__ROOT_URL=https://{{gitea_primary_domain_name}}
networks:
- gitea
- traefik
volumes:
- gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.rule=Host(`git.{{app_domain_name}}`) || Host(`{{gitea_primary_domain_name}}`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]

View File

@ -0,0 +1,54 @@
networks:
portainer:
traefik:
external: true
volumes:
portainer_data:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/portainer/data
name: portainer_data
services:
agent:
image: portainer/agent:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- portainer
environment:
AGENT_SECRET: {{portainer_agent_secret}}
deploy:
mode: global
placement:
constraints: [node.platform.os == linux]
portainer:
image: portainer/portainer:latest
command: "-H tcp://tasks.agent:9001 --tlsskipverify --bind :9000 --tunnel-port 8000 --admin-password {{portainer_htpasswd}}"
ports:
- "9000:9000"
- "8000:8000"
volumes:
- portainer_data:/data
networks:
- portainer
- traefik
environment:
AGENT_SECRET: {{portainer_agent_secret}}
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.{{app_domain_name}}`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]

View File

@ -0,0 +1,31 @@
api:
dashboard: true
insecure: true
providers:
swarm:
exposedByDefault: false
endpoint: unix:///var/run/docker.sock
entrypoints:
web:
address: :80
http:
redirections:
entrypoint:
to: websecure
scheme: https
websecure:
address: :443
certificatesResolvers:
letsencrypt:
acme:
email: {{ cf_email }}
storage: /data/letsencrypt/acme.json
caServer: {{ 'https://acme-v02.api.letsencrypt.org/directory' if traefik_tls_mode == 'production' else 'https://acme-staging-v02.api.letsencrypt.org/directory' }}
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"

View File

@ -0,0 +1,72 @@
networks:
traefik:
driver: overlay
attachable: true
name: traefik
volumes:
traefik:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/traefik
name: traefik
secrets:
cf_dns_api_token:
file: "{{nfs_mount_path}}/traefik/secrets/cf-dns-api-token.secret"
services:
traefik:
image: traefik:v3.3
command:
- "--log.level=DEBUG"
- "--configFile=/data/config/traefik.yml"
ports:
- "80:80"
- "443:443"
- "{{traefik_admin_port}}:8080"
secrets:
- "cf_dns_api_token"
environment:
- "CLOUDFLARE_EMAIL={{cf_email}}"
- "CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik:/data
networks:
- traefik
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.{{app_domain_name}}`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=auth"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.tls.domains[0].main=mnke.org"
- "traefik.http.routers.api.tls.domains[0].sans=*.mnke.org"
- "traefik.http.routers.api.tls.domains[1].main=stingray.mnke.org"
- "traefik.http.routers.api.tls.domains[1].sans=*.stingray.mnke.org"
- "traefik.http.middlewares.auth.basicauth.users={{traefik_htpasswd}}"
# Dummy service for Swarm port detection. The port can be any valid integer value.
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
whoami:
image: "traefik/whoami"
networks:
- traefik
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.stingray.mnke.org`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.swarm.network=traefik"

View File

@ -1,6 +1,47 @@
---
- name: Set up Docker Swarm
hosts: swarm
tags: [swarm-init]
roles:
- docker-swarm
- swarm-bootstrap
- name: Mount extra shares
tags: [extra-shares]
hosts: swarm
vars:
shares:
# I can't seem to use a domain name, so unfortunately, we have to hard-
# code the IP address
- src: //10.0.0.160:/stingray
path: /mnt/stingray-cifs
opts: rw,_netdev,hard,intr,nobrl,username=stingray,password={{ secrets.stingray_password }}
fstype: cifs
state: unmounted
- src: //10.0.0.160:/lfs-media
path: /mnt/media-cifs
opts: rw,_netdev,hard,intr,nobrl,username=mediadm,password={{ secrets.mediadm_password }}
fstype: cifs
state: unmounted
- src: truenas.local:/mnt/emc14t9/lfs/media
path: /mnt/media
opts: rw,_netdev,hard,intr,nolock
fstype: nfs
state: mounted
tasks:
- name: Ensure mount directory exists
become: true
file:
path: "{{ item.path }}"
state: directory
mode: '0755'
loop: "{{ shares }}"
- name: Mount media share
become: true
mount:
src: "{{ item.src }}"
path: "{{ item.path }}"
opts: "{{ item.opts }}"
state: "{{ item.state | default('mounted') }}"
fstype: "{{ item.fstype }}"
loop: "{{ shares }}"

View File

@ -0,0 +1,52 @@
version: '3.8'
networks:
gitea:
driver: overlay
attachable: true
name: gitea
traefik:
external: true
volumes:
gitea:
driver: local
driver_opts:
o: bind
type: none
device: /mnt/stingray/gitea
name: gitea
services:
gitea:
image: docker.io/gitea/gitea:1.23.1
environment:
- USER_UID=1002
- USER_GID=1002
- USER=git
- GITEA_APP_NAME=mnke
- GITEA__server__DOMAIN=git.mnke.org
- GITEA__server__ROOT_URL=https://git.mnke.org
networks:
- gitea
- traefik
volumes:
- gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.rule=Host(`git.mnke.org`) || Host(`git.stingray.mnke.org`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]

View File

@ -0,0 +1,243 @@
---
version: "3.8"
networks:
traefik:
external: true
media:
volumes:
transmission_config:
driver: local
driver_opts:
o: bind
type: none
device: ${TRANSMISSION_CONFIG_DIRECTORY:-/mnt/stingray/media/transmission-openvpn/config}
# Argh, mounting the configuration on the NFS drive is not a good idea!
# The sqlite databases in these volumes can have pretty high random RW, so
# we should keep these local. But fuck, managing them on a non-network drive
# on Swarm is such a pain...
# Maybe we can get these to connect to a different DB?
jellyseerr_config:
driver: local
driver_opts:
o: bind
type: none
device: ${JELLYSEERR_CONFIG_DIRECTORY:-/mnt/stingray/media/jellyseerr/config}
sonarr_config:
driver: local
driver_opts:
o: bind
type: none
device: ${SONARR_CONFIG_DIRECTORY:-/mnt/stingray/media/sonarr/config}
radarr_config:
driver: local
driver_opts:
o: bind
type: none
device: ${RADARR_CONFIG_DIRECTORY:-/mnt/stingray/media/radarr/config}
prowlarr_config:
driver: local
driver_opts:
o: bind
type: none
device: ${PROWLARR_CONFIG_DIRECTORY:-/mnt/stingray/media/prowlarr/config}
services:
transmission-openvpn:
image: haugene/transmission-openvpn
cap_add:
- NET_ADMIN
networks:
- traefik
- media
volumes:
- transmission_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}/torrents:${MEDIA_DIRECTORY:-/mnt/media}/torrents
environment:
- PUID=${PUID:-8796}
- PGID=${PGID:-3005}
- OPENVPN_PROVIDER=${OPENVPN_PROVIDER:-SURFSHARK}
# - OPENVPN_CONFIG=us-sea.prod.surfshark.com_udp
# The startup script fails when cloning and checking out the configuration
# repo for some reason. Passing a URL to skip that seems to work though.
- OPENVPN_CONFIG_URL=${OPENVPN_CONFIG_URL:-https://raw.githubusercontent.com/haugene/vpn-configs-contrib/refs/heads/main/openvpn/surfshark/us-lax.prod.surfshark.com_udp.ovpn}
- OPENVPN_USERNAME=${OPENVPN_USERNAME}
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
# If OpenVPN stops, container will stop and then restart option
# below will bring it back to life.
# Surfshark seems to switch IPs frequently (as of 9/8/2023) so
# this is helpful.
- OPENVPN_OPTS=--inactive 3600 --ping 10 --ping-exit 60
- TRANSMISSION_DOWNLOAD_DIR=${MEDIA_DIRECTORY:-/mnt/media}/torrents/completed
- TRANSMISSION_INCOMPLETE_DIR=${MEDIA_DIRECTORY:-/mnt/media}/torrents/incomplete
- TRANSMISSION_WEB_UI=flood-for-transmission
- TZ=America/Vancouver
- LOCAL_NETWORK=10.0.0.0/16
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.transmission_openvpn.rule=Host(`${TRANSMISSION_HOST:-tovpn.stingray.mnke.org}`)"
- "traefik.http.routers.transmission_openvpn.entrypoints=websecure"
- "traefik.http.routers.transmission_openvpn.tls.certresolver=letsencrypt"
- "traefik.http.services.transmission_openvpn.loadbalancer.server.port=9091"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 64M
prowlarr:
image: lscr.io/linuxserver/prowlarr
environment:
- PUID=${PUID:-8796}
- PGID=${PGID:-3005}
- TZ=America/Vancouver
networks:
- media
- traefik
volumes:
- prowlarr_config:/config
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(`${PROWLARR_HOST:-prowlarr.stingray.mnke.org}`)"
- "traefik.http.routers.prowlarr.entrypoints=websecure"
- "traefik.http.routers.prowlarr.tls.certresolver=letsencrypt"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
- "traefik.swarm.network=traefik"
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
radarr:
image: lscr.io/linuxserver/radarr
environment:
- PUID=${PUID:-8796}
- PGID=${PGID:-3005}
- TZ=America/Vancouver
networks:
- media
- traefik
volumes:
- radarr_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}:${MEDIA_DIRECTORY:-/mnt/media}
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(`${RADARR_HOST:-radarr.stingray.mnke.org}`)"
- "traefik.http.routers.radarr.entrypoints=websecure"
- "traefik.http.routers.radarr.tls.certresolver=letsencrypt"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
sonarr:
image: lscr.io/linuxserver/sonarr
environment:
- PUID=${PUID:-8796}
- PGID=${PGID:-3005}
- TZ=America/Vancouver
networks:
- media
- traefik
volumes:
- sonarr_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}:${MEDIA_DIRECTORY:-/mnt/media}
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(`${SONARR_HOST:-sonarr.stingray.mnke.org}`)"
- "traefik.http.routers.sonarr.entrypoints=websecure"
- "traefik.http.routers.sonarr.tls.certresolver=letsencrypt"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr
environment:
- LOG_LEVEL=${FLARESOLVERR_LOG_LEVEL:-info}
- LOG_HTML=${FLARESOLVERR_LOG_HTML:-false}
- CAPTCHA_SOLVER=${FLARESOLVERR_CAPTCHA_SOLVER:-none}
- TZ=America/Vancouver
networks:
- media
# ports:
# - "${PORT:-8191}:8191"
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.25'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
jellyseerr:
image: fallenbagel/jellyseerr
environment:
- LOG_LEVEL=debug
- TZ=America/Vancouver
networks:
- media
- traefik
volumes:
- jellyseerr_config:/app/config
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(`${JELLYSEERR_HOST:-seerr.stingray.mnke.org}`)"
- "traefik.http.routers.jellyseerr.entrypoints=websecure"
- "traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 64M