diff --git a/compose-files/replicant/mixarr/docker-compose.yml b/compose-files/replicant/mixarr/docker-compose.yml new file mode 100644 index 0000000..37cb607 --- /dev/null +++ b/compose-files/replicant/mixarr/docker-compose.yml @@ -0,0 +1,142 @@ +services: + # ========================================================================= + # MySQL Database + # ========================================================================= + mysql: + image: mysql:8.0 + container_name: mixarr_mysql + hostname: mysql + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - ./mysql_data:/var/lib/mysql + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + networks: + - mixarr_internal + deploy: + resources: + limits: + memory: 1G + cpus: '1.0' + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + labels: + - "autoheal=true" + - "com.centurylinklabs.watchtower.enable=true" + + # ========================================================================= + # Redis Cache + # ========================================================================= + redis: + image: redis:7-alpine + container_name: mixarr_redis + hostname: redis + restart: unless-stopped + volumes: + - ./redis_data:/data + command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru + networks: + - mixarr_internal + deploy: + resources: + limits: + memory: 256M + cpus: '0.5' + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + labels: + - "autoheal=true" + - "com.centurylinklabs.watchtower.enable=true" + + # ========================================================================= + # Mixarr API + # ========================================================================= + api: + image: ghcr.io/aquantumofdonuts/mixarr:latest + container_name: mixarr_api + hostname: api + restart: unless-stopped + ports: + - "3005:3005" + environment: + - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE} + - REDIS_URL=redis://redis:6379 + - SESSION_SECRET=${SESSION_SECRET} + - PORT=3005 + - NODE_ENV=production + volumes: + - ./api_data:/data + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + command: > + sh -c "npx prisma migrate deploy && node apps/api/dist/index.js" + networks: + - mixarr_internal + - proxy + deploy: + resources: + limits: + memory: 512M + cpus: '1.0' + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3005/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 120s + labels: + - "autoheal=true" + - "com.centurylinklabs.watchtower.enable=true" + + # ========================================================================= + # Mixarr Web Frontend + # ========================================================================= + web: + image: ghcr.io/aquantumofdonuts/mixarr:latest + container_name: mixarr_web + hostname: web + restart: unless-stopped + ports: + - "3006:3000" + environment: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + - SESSION_SECRET=${SESSION_SECRET} + - NODE_ENV=production + volumes: + - ./web_data:/data + depends_on: + - api + networks: + - mixarr_internal + - proxy + deploy: + resources: + limits: + memory: 512M + cpus: '1.0' + labels: + - "autoheal=true" + - "com.centurylinklabs.watchtower.enable=true" + - "homepage.group=Media" + - "homepage.name=Mixarr" + - "homepage.icon=lidarr.png" + - "homepage.href=https://mixarr.3ddbrewery.com" + +networks: + mixarr_internal: + driver: bridge + proxy: + external: true diff --git a/playbooks/deploy-mixarr.yml b/playbooks/deploy-mixarr.yml new file mode 100644 index 0000000..9fa48b1 --- /dev/null +++ b/playbooks/deploy-mixarr.yml @@ -0,0 +1,160 @@ +--- +# Deploy Mixarr Stack to replicant VM +# Containers: mysql, redis, api, web +# Target: replicant (192.168.1.80) + +- name: Deploy Mixarr Stack + hosts: replicant + vars: + appdata_path: /home/maddox/docker/appdata + service_name: mixarr + service_dir: "{{ appdata_path }}/{{ service_name }}" + compose_src: "{{ playbook_dir }}/../compose-files/replicant/{{ service_name }}" + + tasks: + # ========================================================================= + # PRE-FLIGHT CHECKS + # ========================================================================= + - name: Check if .env file exists on control server + delegate_to: localhost + ansible.builtin.stat: + path: "{{ compose_src }}/.env" + register: env_file + + - name: Fail if .env is missing + ansible.builtin.fail: + msg: | + .env file not found at {{ compose_src }}/.env + Copy .env.example to .env and fill in the secrets! + when: not env_file.stat.exists + + # ========================================================================= + # DIRECTORY SETUP + # ========================================================================= + - name: Create mixarr base directory + ansible.builtin.file: + path: "{{ service_dir }}" + state: directory + owner: maddox + group: maddox + mode: '0755' + + - name: Create mysql_data directory + ansible.builtin.file: + path: "{{ service_dir }}/mysql_data" + state: directory + owner: maddox + group: maddox + mode: '0755' + + - name: Create redis_data directory + ansible.builtin.file: + path: "{{ service_dir }}/redis_data" + state: directory + owner: maddox + group: maddox + mode: '0755' + + - name: Create api_data directory + ansible.builtin.file: + path: "{{ service_dir }}/api_data" + state: directory + owner: maddox + group: maddox + mode: '0755' + + - name: Create web_data directory + ansible.builtin.file: + path: "{{ service_dir }}/web_data" + state: directory + owner: maddox + group: maddox + mode: '0755' + + # ========================================================================= + # FILE DEPLOYMENT + # ========================================================================= + - name: Copy docker-compose.yml + ansible.builtin.copy: + src: "{{ compose_src }}/docker-compose.yml" + dest: "{{ service_dir }}/docker-compose.yml" + owner: maddox + group: maddox + mode: '0644' + + - name: Copy .env file + ansible.builtin.copy: + src: "{{ compose_src }}/.env" + dest: "{{ service_dir }}/.env" + owner: maddox + group: maddox + mode: '0600' + + # ========================================================================= + # CONTAINER DEPLOYMENT + # ========================================================================= + - name: Deploy mixarr stack + community.docker.docker_compose_v2: + project_src: "{{ service_dir }}" + state: present + pull: always + register: mixarr_result + + - name: Show deployment status + ansible.builtin.debug: + msg: "Mixarr stack deployed: {{ mixarr_result.changed }}" + + # ========================================================================= + # VERIFICATION + # ========================================================================= + - name: Wait for MySQL to be ready + ansible.builtin.command: + cmd: docker exec mixarr_mysql mysqladmin ping -h localhost + register: mysql_check + retries: 30 + delay: 5 + until: mysql_check.rc == 0 + changed_when: false + + - name: Wait for Redis to be ready + ansible.builtin.command: + cmd: docker exec mixarr_redis redis-cli ping + register: redis_check + retries: 10 + delay: 3 + until: "'PONG' in redis_check.stdout" + changed_when: false + + - name: Wait for API to be healthy (may take up to 2 minutes for Prisma migrations) + ansible.builtin.uri: + url: "http://localhost:3005/api/health" + status_code: 200 + timeout: 10 + register: api_health + retries: 30 + delay: 10 + until: api_health.status == 200 + ignore_errors: true + + - name: Wait for Web frontend to be ready + ansible.builtin.uri: + url: "http://localhost:3006" + status_code: [200, 302] + timeout: 10 + register: web_health + retries: 15 + delay: 5 + until: web_health.status in [200, 302] + ignore_errors: true + + - name: Summary + ansible.builtin.debug: + msg: + - "=========================================" + - "Mixarr Stack Deployment Complete" + - "=========================================" + - "✅ MySQL: Running on internal network" + - "✅ Redis: Running on internal network" + - "✅ API: http://192.168.1.80:3005" + - "✅ Web: http://192.168.1.80:3006" + - "========================================="