diff --git a/beoftexas/docker-compose-demo.yaml b/beoftexas/docker-compose-demo.yaml new file mode 100644 index 0000000..cc2fb0e --- /dev/null +++ b/beoftexas/docker-compose-demo.yaml @@ -0,0 +1,52 @@ +name: beoftexas +services: + nginx: + image: nginx:alpine + networks: + shared: + aliases: + - beoftexas-web + default: + volumes: + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - public_files:/var/www/html/public:ro + depends_on: + - web + restart: always + + web: + image: casmith/beoftexas:2026.01.21.30d8b812 + env_file: + - .env + networks: + default: + environment: + DB_NAME: beoftexas + DB_HOST: mariadb + DB_USER: beoftexas + STRAPI_URL: https://demo-strapi.beoftexas.com + volumes: + - public_files:/var/www/html/public + depends_on: + - mariadb + restart: always + + mariadb: + image: mariadb + restart: always + env_file: + - .env + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: beoftexas + MYSQL_USER: beoftexas + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - ./data/mariadb/data:/var/lib/mysql + +volumes: + public_files: + +networks: + shared: + external: true diff --git a/beoftexas/docker/nginx-demo/default.conf b/beoftexas/docker/nginx-demo/default.conf new file mode 100644 index 0000000..5b14a36 --- /dev/null +++ b/beoftexas/docker/nginx-demo/default.conf @@ -0,0 +1,55 @@ +server { + listen 8000; + server_name _; + root /var/www/html/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ajax.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://demo-strapi.beoftexas.com https://ajax.googleapis.com;" always; + + # Serve static files directly + # Note: add_header in location blocks overrides parent headers, so we repeat security headers here + location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + add_header Cross-Origin-Resource-Policy "same-origin" always; + try_files $uri =404; + } + + # Main location block + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM proxy + location ~ \.php$ { + fastcgi_pass web:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # FastCGI settings + fastcgi_connect_timeout 60; + fastcgi_send_timeout 180; + fastcgi_read_timeout 180; + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } +} diff --git a/beoftexas/docker/nginx/default.conf b/beoftexas/docker/nginx/default.conf index 7c5b424..e103956 100644 --- a/beoftexas/docker/nginx/default.conf +++ b/beoftexas/docker/nginx/default.conf @@ -11,7 +11,7 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; add_header Cross-Origin-Resource-Policy "same-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://strapi.beoftexas.com;" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ajax.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://strapi.beoftexas.com https://ajax.googleapis.com;" always; # Serve static files directly # Note: add_header in location blocks overrides parent headers, so we repeat security headers here diff --git a/beoftexas/playbook-demo.yaml b/beoftexas/playbook-demo.yaml new file mode 100644 index 0000000..adff81c --- /dev/null +++ b/beoftexas/playbook-demo.yaml @@ -0,0 +1,89 @@ +- name: Deploy Benefit Elect of Texas website via Docker (Demo) + hosts: demo + become: true + tasks: + - name: Log into DockerHub + docker_login: + username: '{{ lookup("env", "DOCKERHUB_USERNAME") }}' + password: '{{ lookup("env", "DOCKERHUB_PASSWORD") }}' + + - name: Creates directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: ubuntu + group: ubuntu + loop: + - "{{ deploy_path }}" + - "{{ deploy_path }}/beoftexas" + - "{{ deploy_path }}/beoftexas/data" + - "{{ deploy_path }}/beoftexas/docker/nginx" + + - name: Create shared Docker network + community.docker.docker_network: + name: shared + state: present + + - name: copy Docker Compose files + copy: + src: "docker-compose-demo.yaml" + dest: "{{ deploy_path }}/beoftexas/docker-compose.yaml" + owner: ubuntu + group: ubuntu + + - name: copy nginx config (demo version with updated CSP) + copy: + src: "docker/nginx-demo/default.conf" + dest: "{{ deploy_path }}/beoftexas/docker/nginx/default.conf" + owner: ubuntu + group: ubuntu + + - name: copy environment file + copy: + src: "../secret/demo/beoftexas.env" + dest: "{{ deploy_path }}/beoftexas/.env" + mode: '0600' + owner: ubuntu + group: ubuntu + + - name: Deploy with docker compose + ansible.builtin.shell: + cmd: "docker compose up -d" + chdir: "{{ deploy_path }}/beoftexas/" + register: compose_up_result + + - name: Gracefully reload beoftexas nginx config + ansible.builtin.shell: + cmd: "docker compose exec -T nginx nginx -s reload" + chdir: "{{ deploy_path }}/beoftexas/" + register: nginx_reload_result + failed_when: false + + - name: Wait for PHP-FPM container to be ready + ansible.builtin.shell: + cmd: "docker compose exec -T web php -v" + chdir: "{{ deploy_path }}/beoftexas/" + register: php_check + retries: 10 + delay: 3 + until: php_check.rc == 0 + + - name: Wait for MariaDB to be ready + ansible.builtin.shell: + cmd: "docker compose exec -T mariadb mariadb -u beoftexas -p$(grep '^DB_PASSWORD=' .env | cut -d'=' -f2-) -e 'SELECT 1' 2>&1" + chdir: "{{ deploy_path }}/beoftexas/" + register: db_check + retries: 30 + delay: 2 + until: db_check.rc == 0 + + # NOTE: Migrations are run after database restore in deploy-demo.sh + # Do not run migrations here on empty database + + - name: Restart reverse proxy nginx to pick up beoftexas on shared network + ansible.builtin.shell: + cmd: "docker compose restart web" + chdir: "{{ deploy_path }}/nginx-demo" + register: nginx_restart_result + failed_when: false + changed_when: nginx_restart_result.rc == 0 diff --git a/beoftexas/restore-db-demo.yaml b/beoftexas/restore-db-demo.yaml new file mode 100644 index 0000000..7d9a4c0 --- /dev/null +++ b/beoftexas/restore-db-demo.yaml @@ -0,0 +1,174 @@ +--- +- name: Force restore beoftexas database from backup (Demo) + hosts: demo + gather_facts: false + become: true + vars: + deploy_path: /home/ubuntu/deploy/beoftexas + s3_access_key: "{{ lookup('env', 'CONTABO_S3_ACCESS_KEY') }}" + s3_secret_key: "{{ lookup('env', 'CONTABO_S3_SECRET_KEY') }}" + s3_bucket: "{{ lookup('env', 'CONTABO_S3_BUCKET') | default('beoftexas-backup', true) }}" + env_file: "{{ deploy_path }}/.env" + + tasks: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Install unzip utility + ansible.builtin.apt: + name: unzip + state: present + + - name: Check if AWS CLI is already installed + ansible.builtin.command: aws --version + register: aws_cli_check + changed_when: false + failed_when: false + + - name: Download AWS CLI v2 installer + ansible.builtin.get_url: + url: https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip + dest: /tmp/awscliv2.zip + mode: '0644' + when: aws_cli_check.rc != 0 + + - name: Unzip AWS CLI installer + ansible.builtin.unarchive: + src: /tmp/awscliv2.zip + dest: /tmp/ + remote_src: true + when: aws_cli_check.rc != 0 + + - name: Install AWS CLI v2 + ansible.builtin.command: /tmp/aws/install + when: aws_cli_check.rc != 0 + + - name: Clean up AWS CLI installer files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /tmp/awscliv2.zip + - /tmp/aws + when: aws_cli_check.rc != 0 + + - name: Download database backup from Contabo S3 + ansible.builtin.shell: | + AWS_ACCESS_KEY_ID="{{ s3_access_key }}" \ + AWS_SECRET_ACCESS_KEY="{{ s3_secret_key }}" \ + aws s3 cp s3://{{ s3_bucket }}/beoftexas_latest.sql.gz /tmp/beoftexas_latest.sql.gz \ + --endpoint-url https://usc1.contabostorage.com + no_log: false + changed_when: true + + - name: Verify backup file was downloaded + ansible.builtin.stat: + path: /tmp/beoftexas_latest.sql.gz + register: backup_file + failed_when: not backup_file.stat.exists or backup_file.stat.size == 0 + + - name: Display backup file info + ansible.builtin.debug: + msg: "Backup file downloaded: {{ backup_file.stat.size }} bytes" + + - name: Stop beoftexas application service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose stop web + register: stop_result + changed_when: true + + - name: Display stop result + ansible.builtin.debug: + msg: "Beoftexas service stopped" + + - name: Wait for MariaDB to be ready + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose exec -T mariadb mariadb -u root -p$(grep "^DB_ROOT_PASSWORD=" {{ env_file }} | cut -d'=' -f2-) -e "SELECT 1" 2>&1 + register: db_ready + until: db_ready.rc == 0 + retries: 30 + delay: 2 + changed_when: false + no_log: false + + - name: Drop existing beoftexas database + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose exec -T mariadb mariadb -u root -p$(grep "^DB_ROOT_PASSWORD=" {{ env_file }} | cut -d'=' -f2-) -e "DROP DATABASE IF EXISTS beoftexas;" + register: drop_result + changed_when: true + no_log: true + + - name: Display database drop status + ansible.builtin.debug: + msg: "Database dropped successfully" + + - name: Create fresh beoftexas database + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose exec -T mariadb mariadb -u root -p$(grep "^DB_ROOT_PASSWORD=" {{ env_file }} | cut -d'=' -f2-) -e "CREATE DATABASE beoftexas CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + register: create_result + changed_when: true + no_log: true + + - name: Display database creation status + ansible.builtin.debug: + msg: "Database created successfully" + + - name: Test backup file integrity + ansible.builtin.shell: gunzip -t /tmp/beoftexas_latest.sql.gz + register: integrity_check + changed_when: false + failed_when: integrity_check.rc != 0 + + - name: Restore database from backup + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + gunzip -c /tmp/beoftexas_latest.sql.gz | docker compose exec -T mariadb mariadb -u root -p$(grep "^DB_ROOT_PASSWORD=" {{ env_file }} | cut -d'=' -f2-) beoftexas + register: restore_result + changed_when: true + no_log: false + + - name: Display restore completion + ansible.builtin.debug: + msg: "Database restoration completed" + + - name: Verify database has tables after restore + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose exec -T mariadb mariadb -u root -p$(grep "^DB_ROOT_PASSWORD=" {{ env_file }} | cut -d'=' -f2-) beoftexas -e "SHOW TABLES;" 2>/dev/null | wc -l + register: table_count + changed_when: false + no_log: true + failed_when: (table_count.stdout | int) <= 1 + + - name: Display table count + ansible.builtin.debug: + msg: "Database restored successfully with {{ (table_count.stdout | int) - 1 }} tables" + + - name: Restart beoftexas application service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + docker compose start web + register: start_result + changed_when: true + + - name: Display restart status + ansible.builtin.debug: + msg: "Beoftexas service restarted successfully" + + - name: Clean up temporary backup file + ansible.builtin.file: + path: /tmp/beoftexas_latest.sql.gz + state: absent + + - name: Display final status + ansible.builtin.debug: + msg: + - "=====================================" + - "Database restore completed successfully!" + - "=====================================" diff --git a/cloudflared/docker-compose.yaml b/cloudflared/docker-compose.yaml new file mode 100644 index 0000000..a63a911 --- /dev/null +++ b/cloudflared/docker-compose.yaml @@ -0,0 +1,14 @@ +name: cloudflared +services: + tunnel: + image: cloudflare/cloudflared:latest + command: tunnel --no-autoupdate run + environment: + - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - shared + restart: always + +networks: + shared: + external: true diff --git a/cloudflared/playbook.yaml b/cloudflared/playbook.yaml new file mode 100644 index 0000000..a01aff0 --- /dev/null +++ b/cloudflared/playbook.yaml @@ -0,0 +1,53 @@ +- name: Deploy Cloudflare Tunnel via Docker + hosts: demo + become: true + tasks: + - name: Creates directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + owner: ubuntu + group: ubuntu + loop: + - "{{ deploy_path }}/cloudflared" + - "{{ deploy_path }}/cloudflared/secrets" + + - name: Create shared Docker network + community.docker.docker_network: + name: shared + state: present + + - name: Copy Docker Compose files + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ deploy_path }}/cloudflared/{{ item }}" + mode: "0644" + owner: ubuntu + group: ubuntu + loop: + - docker-compose.yaml + + - name: Copy tunnel token secret + ansible.builtin.copy: + src: "../secret/demo/cloudflare_tunnel_token.txt" + dest: "{{ deploy_path }}/cloudflared/secrets/cloudflare_tunnel_token.txt" + mode: "0600" + owner: ubuntu + group: ubuntu + + - name: Deploy with docker compose + ansible.builtin.shell: + cmd: "export CLOUDFLARE_TUNNEL_TOKEN=$(cat secrets/cloudflare_tunnel_token.txt) && docker compose up -d" + chdir: "{{ deploy_path }}/cloudflared/" + register: compose_up_result + + - name: Display cloudflared status + ansible.builtin.shell: + cmd: "docker compose logs --tail=20" + chdir: "{{ deploy_path }}/cloudflared/" + register: tunnel_logs + + - name: Show tunnel logs + ansible.builtin.debug: + var: tunnel_logs.stdout_lines diff --git a/deploy-demo.sh b/deploy-demo.sh new file mode 100755 index 0000000..d0d964d --- /dev/null +++ b/deploy-demo.sh @@ -0,0 +1,172 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +INVENTORY="hosts-demo.yaml" + +# Get server IP from inventory or use default +SERVER_IP="${1:-$(grep 'ansible_host:' $INVENTORY | head -1 | awk '{print $2}')}" + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Demo Environment Deployment Script${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "Target server: $SERVER_IP" +echo "Inventory: $INVENTORY" +echo "" + +# Function to run a playbook with nice output +run_playbook() { + local playbook=$1 + local description=$2 + + echo -e "${YELLOW}>>> $description${NC}" + echo "Running: ansible-playbook -i $INVENTORY $playbook" + + if ansible-playbook -i "$INVENTORY" "$playbook"; then + echo -e "${GREEN}✓ $description completed${NC}" + echo "" + else + echo -e "${RED}✗ $description failed${NC}" + exit 1 + fi +} + +# Function to run SSH command on demo server +run_ssh() { + ssh -o BatchMode=yes ubuntu@$SERVER_IP "$@" +} + +# Check prerequisites +echo -e "${YELLOW}>>> Checking prerequisites...${NC}" + +# Check if secret files have been configured +if grep -q "YOUR_CLOUDFLARE_API_TOKEN_HERE" secret/demo/cloudflare.ini 2>/dev/null; then + echo -e "${RED}ERROR: Cloudflare API token not configured${NC}" + echo "Please edit secret/demo/cloudflare.ini with your Cloudflare API token" + exit 1 +fi + +if grep -q "YOUR_CLOUDFLARE_TUNNEL_TOKEN_HERE" secret/demo/cloudflare_tunnel_token.txt 2>/dev/null; then + echo -e "${RED}ERROR: Cloudflare tunnel token not configured${NC}" + echo "Please edit secret/demo/cloudflare_tunnel_token.txt with your tunnel token" + exit 1 +fi + +# Check S3 credentials for database restores +if [ -z "$CONTABO_S3_ACCESS_KEY" ] || [ -z "$CONTABO_S3_SECRET_KEY" ]; then + echo -e "${RED}ERROR: S3 credentials not set${NC}" + echo "Please set environment variables:" + echo " export CONTABO_S3_ACCESS_KEY=your_access_key" + echo " export CONTABO_S3_SECRET_KEY=your_secret_key" + exit 1 +fi + +echo -e "${GREEN}✓ Prerequisites check passed${NC}" +echo "" + +# Check if we need to bootstrap (use root password) +echo -e "${YELLOW}>>> Checking if server is bootstrapped...${NC}" +if ! ssh -o BatchMode=yes -o ConnectTimeout=5 ubuntu@$SERVER_IP exit 2>/dev/null; then + echo "Server needs bootstrapping (will prompt for root password)" + echo -e "${YELLOW}>>> Step 1: Bootstrap server (create ubuntu user, SSH keys, disable password auth)${NC}" + ansible-playbook -i "$SERVER_IP," bootstrap.yaml --ask-pass + echo -e "${GREEN}✓ Bootstrap completed${NC}" + echo "" +else + echo "Server already bootstrapped, skipping..." + echo "" +fi + +# Deploy infrastructure +run_playbook "install-docker.yaml" "Step 2: Install Docker" + +# Deploy Strapi +run_playbook "strapi/playbook-demo.yaml" "Step 3: Deploy Strapi (PostgreSQL CMS)" + +# Restore Strapi data +run_playbook "strapi/restore-app-demo.yaml" "Step 4: Restore Strapi app directory" +run_playbook "strapi/restore-db-demo.yaml" "Step 5: Restore Strapi database" + +# Restart Strapi to pick up restored data +echo -e "${YELLOW}>>> Step 6: Restart Strapi${NC}" +run_ssh "cd /home/ubuntu/deploy/strapi && export DATABASE_PASSWORD=\$(cat secrets/strapi_db_pw.txt) && docker compose restart web" +echo -e "${GREEN}✓ Strapi restarted${NC}" +echo "" + +# Clear beoftexas public_files volume if it exists (for clean asset deployment) +echo -e "${YELLOW}>>> Step 7: Prepare beoftexas deployment${NC}" +run_ssh "sudo docker volume rm beoftexas_public_files 2>/dev/null || true" +echo -e "${GREEN}✓ beoftexas volume cleared${NC}" +echo "" + +# Deploy beoftexas +run_playbook "beoftexas/playbook-demo.yaml" "Step 8: Deploy Benefit Elect of Texas" + +# Restore beoftexas database +run_playbook "beoftexas/restore-db-demo.yaml" "Step 9: Restore beoftexas database" + +# Run beoftexas migrations (after restore) +echo -e "${YELLOW}>>> Step 10: Run beoftexas database migrations${NC}" +run_ssh "cd /home/ubuntu/deploy/beoftexas && docker compose exec -T web php vendor/bin/phinx migrate -e production" +echo -e "${GREEN}✓ beoftexas migrations completed${NC}" +echo "" + +# Deploy wbaoftexas +run_playbook "wbaoftexas/playbook-demo.yaml" "Step 11: Deploy WBA of Texas" + +# Note: wbaoftexas restore playbook would go here if needed +# run_playbook "wbaoftexas/restore-db-demo.yaml" "Step 12: Restore wbaoftexas database" + +# Restore SSL certificates from S3 if backup exists (avoids Let's Encrypt rate limits) +echo -e "${YELLOW}>>> Step 12: Restore SSL certificates from S3 (if backup exists)${NC}" +S3_BUCKET="${CONTABO_S3_BUCKET:-beoftexas-backup}" +run_ssh "mkdir -p /home/ubuntu/deploy/nginx-demo/data/certbot +if AWS_ACCESS_KEY_ID='$CONTABO_S3_ACCESS_KEY' \ + AWS_SECRET_ACCESS_KEY='$CONTABO_S3_SECRET_KEY' \ + aws s3 cp s3://$S3_BUCKET/demo-certbot-backup.tar.gz /tmp/demo-certbot-backup.tar.gz \ + --endpoint-url https://usc1.contabostorage.com 2>/dev/null; then + cd /home/ubuntu/deploy/nginx-demo/data/certbot + tar -xzf /tmp/demo-certbot-backup.tar.gz + rm /tmp/demo-certbot-backup.tar.gz + echo 'Certificates restored from S3 backup' +else + echo 'No certificate backup found in S3, will issue new certificates' +fi" +echo -e "${GREEN}✓ Certificate restore check complete${NC}" +echo "" + +# Deploy nginx and cloudflared +run_playbook "nginx-demo/playbook.yaml" "Step 13: Deploy Nginx (reverse proxy + SSL certs)" +run_playbook "cloudflared/playbook.yaml" "Step 14: Deploy Cloudflare Tunnel" + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Demo Deployment Complete!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "All services deployed and databases restored!" +echo "" +echo "Deployed services:" +echo " ✓ Strapi CMS (PostgreSQL) - restored from backup" +echo " ✓ Benefit Elect of Texas (MariaDB) - restored from backup" +echo " ✓ WBA of Texas (MariaDB)" +echo " ✓ Nginx reverse proxy with Let's Encrypt SSL" +echo " ✓ Cloudflare Tunnel" +echo "" +echo "Demo URLs:" +echo " - https://demo.beoftexas.com" +echo " - https://demo-strapi.beoftexas.com" +echo " - https://demo.wbaoftexas.com" +echo "" +echo "Verification steps:" +echo " 1. SSH to demo VM and check containers: docker ps" +echo " 2. Check cloudflared logs: docker logs cloudflared-tunnel-1" +echo " 3. Verify certs: ls /home/ubuntu/deploy/nginx-demo/data/certbot/conf/live/" +echo " 4. Test URLs in browser" +echo "" diff --git a/hosts-demo.yaml b/hosts-demo.yaml new file mode 100644 index 0000000..c0d746b --- /dev/null +++ b/hosts-demo.yaml @@ -0,0 +1,10 @@ +all: + vars: + ansible_python_interpreter: /usr/bin/python3 + children: + servers: + hosts: + demo: + ansible_host: 192.168.10.43 + ansible_user: ubuntu + deploy_path: /home/ubuntu/deploy diff --git a/nginx-demo/conf/demo-strapi.beoftexas.com.conf b/nginx-demo/conf/demo-strapi.beoftexas.com.conf new file mode 100644 index 0000000..e410b28 --- /dev/null +++ b/nginx-demo/conf/demo-strapi.beoftexas.com.conf @@ -0,0 +1,37 @@ +server { + listen 80; + listen [::]:80; + + server_name demo-strapi.beoftexas.com; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://demo-strapi.beoftexas.com$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name demo-strapi.beoftexas.com; + + ssl_certificate /etc/letsencrypt/live/demo.beoftexas.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/demo.beoftexas.com/privkey.pem; + + # Use Docker's internal DNS server for runtime resolution + resolver 127.0.0.11 valid=10s; + + location / { + set $backend "strapi-web:1337"; + proxy_pass http://$backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx-demo/conf/demo.beoftexas.com.conf b/nginx-demo/conf/demo.beoftexas.com.conf new file mode 100644 index 0000000..2bed857 --- /dev/null +++ b/nginx-demo/conf/demo.beoftexas.com.conf @@ -0,0 +1,37 @@ +server { + listen 80; + listen [::]:80; + + server_name demo.beoftexas.com; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://demo.beoftexas.com$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name demo.beoftexas.com; + + ssl_certificate /etc/letsencrypt/live/demo.beoftexas.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/demo.beoftexas.com/privkey.pem; + + # Use Docker's internal DNS server for runtime resolution + resolver 127.0.0.11 valid=10s; + + location / { + set $backend "beoftexas-web:8000"; + proxy_pass http://$backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx-demo/conf/demo.wbaoftexas.com.conf b/nginx-demo/conf/demo.wbaoftexas.com.conf new file mode 100644 index 0000000..5fb2346 --- /dev/null +++ b/nginx-demo/conf/demo.wbaoftexas.com.conf @@ -0,0 +1,37 @@ +server { + listen 80; + listen [::]:80; + + server_name demo.wbaoftexas.com; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://demo.wbaoftexas.com$request_uri; + } +} + +server { + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2; + + server_name demo.wbaoftexas.com; + + ssl_certificate /etc/letsencrypt/live/demo.beoftexas.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/demo.beoftexas.com/privkey.pem; + + # Use Docker's internal DNS server for runtime resolution + resolver 127.0.0.11 valid=10s; + + location / { + set $backend "wbaoftexas-web:8000"; + proxy_pass http://$backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx-demo/docker-compose.yaml b/nginx-demo/docker-compose.yaml new file mode 100644 index 0000000..ea44923 --- /dev/null +++ b/nginx-demo/docker-compose.yaml @@ -0,0 +1,27 @@ +name: nginx-demo +services: + web: + image: nginx:1.25-alpine + networks: + shared: + aliases: + - nginx-web + default: + volumes: + - ./data/nginx:/etc/nginx/conf.d + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot + restart: always + + certbot: + image: certbot/dns-cloudflare + volumes: + - ./data/cloudflare.ini:/etc/cloudflare.ini:ro + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + restart: always + +networks: + shared: + external: true diff --git a/nginx-demo/playbook.yaml b/nginx-demo/playbook.yaml new file mode 100644 index 0000000..d3feed1 --- /dev/null +++ b/nginx-demo/playbook.yaml @@ -0,0 +1,94 @@ +- name: Deploy nginx reverse proxy with certbot DNS-01 via Docker + hosts: demo + become: true + vars: + cert_email: admin@beoftexas.com + demo_domains: + - demo.beoftexas.com + - demo-strapi.beoftexas.com + - demo.wbaoftexas.com + tasks: + - name: Creates directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + owner: ubuntu + group: ubuntu + loop: + - "{{ deploy_path }}/nginx-demo" + - "{{ deploy_path }}/nginx-demo/data" + - "{{ deploy_path }}/nginx-demo/data/nginx" + - "{{ deploy_path }}/nginx-demo/data/certbot" + - "{{ deploy_path }}/nginx-demo/data/certbot/conf" + - "{{ deploy_path }}/nginx-demo/data/certbot/www" + + - name: Create shared Docker network + community.docker.docker_network: + name: shared + state: present + + - name: Copy Docker Compose files + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ deploy_path }}/nginx-demo/{{ item }}" + mode: "0644" + owner: ubuntu + group: ubuntu + loop: + - docker-compose.yaml + + - name: Copy Cloudflare credentials for DNS-01 challenge + ansible.builtin.copy: + src: "../secret/demo/cloudflare.ini" + dest: "{{ deploy_path }}/nginx-demo/data/cloudflare.ini" + mode: "0600" + owner: ubuntu + group: ubuntu + + - name: Copy all nginx config files from conf directory + ansible.builtin.copy: + src: "conf/" + dest: "{{ deploy_path }}/nginx-demo/data/nginx/" + mode: "0644" + owner: ubuntu + group: ubuntu + + - name: Check if certificates already exist + ansible.builtin.stat: + path: "{{ deploy_path }}/nginx-demo/data/certbot/conf/live/demo.beoftexas.com/fullchain.pem" + register: cert_stat + + - name: Issue SSL certificates via DNS-01 challenge + ansible.builtin.shell: + cmd: | + docker compose run --rm --entrypoint '' certbot certbot certonly \ + --dns-cloudflare \ + --dns-cloudflare-credentials /etc/cloudflare.ini \ + --dns-cloudflare-propagation-seconds 60 \ + -d {{ demo_domains | join(' -d ') }} \ + --agree-tos \ + --email {{ cert_email }} \ + --non-interactive + chdir: "{{ deploy_path }}/nginx-demo/" + register: certbot_result + when: not cert_stat.stat.exists + + - name: Display certbot output + ansible.builtin.debug: + var: certbot_result.stdout_lines + when: certbot_result is defined and certbot_result.stdout_lines is defined + + - name: Deploy with docker compose + ansible.builtin.shell: + cmd: "docker compose up -d" + chdir: "{{ deploy_path }}/nginx-demo/" + register: compose_up_result + + - name: Gracefully reload nginx to apply config changes + ansible.builtin.shell: + cmd: "docker compose exec -T web nginx -s reload" + chdir: "{{ deploy_path }}/nginx-demo/" + register: nginx_reload_result + failed_when: false + changed_when: nginx_reload_result.rc == 0 diff --git a/secret/demo/beoftexas.env b/secret/demo/beoftexas.env new file mode 100644 index 0000000..ca3e865 Binary files /dev/null and b/secret/demo/beoftexas.env differ diff --git a/secret/demo/cloudflare.ini b/secret/demo/cloudflare.ini new file mode 100644 index 0000000..a07b8a8 Binary files /dev/null and b/secret/demo/cloudflare.ini differ diff --git a/secret/demo/cloudflare_tunnel_token.txt b/secret/demo/cloudflare_tunnel_token.txt new file mode 100644 index 0000000..a5f3006 Binary files /dev/null and b/secret/demo/cloudflare_tunnel_token.txt differ diff --git a/secret/demo/strapi_db_pw.txt b/secret/demo/strapi_db_pw.txt new file mode 100644 index 0000000..8cb27bc Binary files /dev/null and b/secret/demo/strapi_db_pw.txt differ diff --git a/secret/demo/wbaoftexas_db_pw.txt b/secret/demo/wbaoftexas_db_pw.txt new file mode 100644 index 0000000..1250a70 Binary files /dev/null and b/secret/demo/wbaoftexas_db_pw.txt differ diff --git a/secret/demo/wbaoftexas_db_root_pw.txt b/secret/demo/wbaoftexas_db_root_pw.txt new file mode 100644 index 0000000..69b8497 Binary files /dev/null and b/secret/demo/wbaoftexas_db_root_pw.txt differ diff --git a/strapi/docker-compose.yaml b/strapi/docker-compose.yaml index 1c4279a..96fcab9 100644 --- a/strapi/docker-compose.yaml +++ b/strapi/docker-compose.yaml @@ -8,6 +8,7 @@ services: DATABASE_HOST: postgres DATABASE_PORT: 5432 DATABASE_USERNAME: strapi + DATABASE_PASSWORD: ${DATABASE_PASSWORD} networks: shared: aliases: diff --git a/strapi/playbook-demo.yaml b/strapi/playbook-demo.yaml new file mode 100644 index 0000000..de071fa --- /dev/null +++ b/strapi/playbook-demo.yaml @@ -0,0 +1,58 @@ +- name: Deploy Strapi via Docker (Demo) + hosts: demo + become: true + tasks: + - name: Log into DockerHub + docker_login: + username: '{{ lookup("env", "DOCKERHUB_USERNAME") }}' + password: '{{ lookup("env", "DOCKERHUB_PASSWORD") }}' + + - name: Creates directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: ubuntu + group: ubuntu + loop: + - "{{ deploy_path }}" + - "{{ deploy_path }}/strapi" + - "{{ deploy_path }}/strapi/data" + - "{{ deploy_path }}/strapi/secrets" + + - name: Create shared Docker network + community.docker.docker_network: + name: shared + state: present + + - name: copy Docker Compose files + copy: + src: "{{ item }}" + dest: "{{ deploy_path }}/strapi/{{ item }}" + owner: ubuntu + group: ubuntu + loop: + - docker-compose.yaml + + - name: copy secrets + copy: + src: "../secret/demo/{{ item }}" + dest: "{{ deploy_path }}/strapi/secrets/{{ item }}" + owner: ubuntu + group: ubuntu + mode: '0600' + loop: + - strapi_db_pw.txt + + - name: Deploy with docker compose + ansible.builtin.shell: + cmd: "export DATABASE_PASSWORD=$(cat secrets/strapi_db_pw.txt) && docker compose up -d" + chdir: "{{ deploy_path }}/strapi/" + register: compose_up_result + + - name: Restart nginx to pick up strapi on shared network + ansible.builtin.shell: + cmd: "docker compose restart web" + chdir: "{{ deploy_path }}/nginx-demo" + register: nginx_restart_result + failed_when: false + changed_when: nginx_restart_result.rc == 0 diff --git a/strapi/restore-app-demo.yaml b/strapi/restore-app-demo.yaml new file mode 100644 index 0000000..fc2ff34 --- /dev/null +++ b/strapi/restore-app-demo.yaml @@ -0,0 +1,140 @@ +--- +- name: Restore Strapi app directory from S3 backup (Demo) + hosts: demo + gather_facts: false + become: true + vars: + deploy_path: /home/ubuntu/deploy/strapi + s3_access_key: "{{ lookup('env', 'CONTABO_S3_ACCESS_KEY') }}" + s3_secret_key: "{{ lookup('env', 'CONTABO_S3_SECRET_KEY') }}" + s3_bucket: "{{ lookup('env', 'CONTABO_S3_BUCKET') | default('beoftexas-backup', true) }}" + + tasks: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Check if AWS CLI is already installed + ansible.builtin.command: aws --version + register: aws_cli_check + changed_when: false + failed_when: false + + - name: Download AWS CLI v2 installer + ansible.builtin.get_url: + url: https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip + dest: /tmp/awscliv2.zip + mode: '0644' + when: aws_cli_check.rc != 0 + + - name: Unzip AWS CLI installer + ansible.builtin.unarchive: + src: /tmp/awscliv2.zip + dest: /tmp/ + remote_src: true + when: aws_cli_check.rc != 0 + + - name: Install AWS CLI v2 + ansible.builtin.command: /tmp/aws/install + when: aws_cli_check.rc != 0 + + - name: Clean up AWS CLI installer files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /tmp/awscliv2.zip + - /tmp/aws + when: aws_cli_check.rc != 0 + + - name: Download Strapi app backup from Contabo S3 + ansible.builtin.shell: | + AWS_ACCESS_KEY_ID="{{ s3_access_key }}" \ + AWS_SECRET_ACCESS_KEY="{{ s3_secret_key }}" \ + aws s3 cp s3://{{ s3_bucket }}/strapi_app_latest.tar.gz /tmp/strapi_app_latest.tar.gz \ + --endpoint-url https://usc1.contabostorage.com + no_log: false + changed_when: true + + - name: Verify backup file was downloaded + ansible.builtin.stat: + path: /tmp/strapi_app_latest.tar.gz + register: backup_file + failed_when: not backup_file.stat.exists or backup_file.stat.size == 0 + + - name: Display backup file info + ansible.builtin.debug: + msg: "Backup file downloaded: {{ backup_file.stat.size }} bytes" + + - name: Stop Strapi service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + export DATABASE_PASSWORD=$(cat secrets/strapi_db_pw.txt) && \ + docker compose stop web + register: stop_result + changed_when: true + + - name: Display stop result + ansible.builtin.debug: + msg: "Strapi service stopped" + + - name: Backup existing app directory (if exists) + ansible.builtin.shell: | + if [ -d "{{ deploy_path }}/app" ]; then + mv {{ deploy_path }}/app {{ deploy_path }}/app.backup.$(date +%Y%m%d_%H%M%S) + fi + args: + executable: /bin/bash + changed_when: true + + - name: Test backup file integrity + ansible.builtin.shell: tar -tzf /tmp/strapi_app_latest.tar.gz > /dev/null + register: integrity_check + changed_when: false + failed_when: integrity_check.rc != 0 + + - name: Extract Strapi app directory from backup + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + tar -xzf /tmp/strapi_app_latest.tar.gz + register: extract_result + changed_when: true + + - name: Display extraction completion + ansible.builtin.debug: + msg: "Strapi app directory extracted successfully" + + - name: Verify app directory exists + ansible.builtin.stat: + path: "{{ deploy_path }}/app" + register: app_dir + failed_when: not app_dir.stat.exists or not app_dir.stat.isdir + + - name: Display app directory status + ansible.builtin.debug: + msg: "App directory verified at {{ deploy_path }}/app" + + - name: Restart Strapi service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + export DATABASE_PASSWORD=$(cat secrets/strapi_db_pw.txt) && \ + docker compose start web + register: start_result + changed_when: true + + - name: Display restart status + ansible.builtin.debug: + msg: "Strapi service restarted successfully" + + - name: Clean up temporary backup file + ansible.builtin.file: + path: /tmp/strapi_app_latest.tar.gz + state: absent + + - name: Display final status + ansible.builtin.debug: + msg: + - "=====================================" + - "Strapi app restore completed successfully!" + - "=====================================" diff --git a/strapi/restore-db-demo.yaml b/strapi/restore-db-demo.yaml new file mode 100644 index 0000000..b67f525 --- /dev/null +++ b/strapi/restore-db-demo.yaml @@ -0,0 +1,186 @@ +--- +- name: Restore strapi postgres database from S3 backup (Demo) + hosts: demo + gather_facts: false + become: true + vars: + deploy_path: /home/ubuntu/deploy/strapi + s3_access_key: "{{ lookup('env', 'CONTABO_S3_ACCESS_KEY') }}" + s3_secret_key: "{{ lookup('env', 'CONTABO_S3_SECRET_KEY') }}" + s3_bucket: "{{ lookup('env', 'CONTABO_S3_BUCKET') | default('beoftexas-backup', true) }}" + db_pw_file: "{{ deploy_path }}/secrets/strapi_db_pw.txt" + + tasks: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Install unzip utility + ansible.builtin.apt: + name: unzip + state: present + + - name: Check if AWS CLI is already installed + ansible.builtin.command: aws --version + register: aws_cli_check + changed_when: false + failed_when: false + + - name: Download AWS CLI v2 installer + ansible.builtin.get_url: + url: https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip + dest: /tmp/awscliv2.zip + mode: '0644' + when: aws_cli_check.rc != 0 + + - name: Unzip AWS CLI installer + ansible.builtin.unarchive: + src: /tmp/awscliv2.zip + dest: /tmp/ + remote_src: true + when: aws_cli_check.rc != 0 + + - name: Install AWS CLI v2 + ansible.builtin.command: /tmp/aws/install + when: aws_cli_check.rc != 0 + + - name: Clean up AWS CLI installer files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /tmp/awscliv2.zip + - /tmp/aws + when: aws_cli_check.rc != 0 + + # NOTE: strapi_latest.sql.gz is corrupted (see GitHub issue #9) + # Using last known good backup from 2026-01-09 until backup job is fixed + - name: Download strapi database backup from Contabo S3 + ansible.builtin.shell: | + AWS_ACCESS_KEY_ID="{{ s3_access_key }}" \ + AWS_SECRET_ACCESS_KEY="{{ s3_secret_key }}" \ + aws s3 cp s3://{{ s3_bucket }}/strapi_20260109T231005.sql.gz /tmp/strapi_postgres_latest.sql.gz \ + --endpoint-url https://usc1.contabostorage.com + no_log: false + changed_when: true + + - name: Verify backup file was downloaded + ansible.builtin.stat: + path: /tmp/strapi_postgres_latest.sql.gz + register: backup_file + failed_when: not backup_file.stat.exists or backup_file.stat.size == 0 + + - name: Display backup file info + ansible.builtin.debug: + msg: "Backup file downloaded: {{ backup_file.stat.size }} bytes" + + - name: Decompress backup file + ansible.builtin.shell: gunzip -f /tmp/strapi_postgres_latest.sql.gz + register: decompress_result + changed_when: true + + - name: Verify decompressed file exists + ansible.builtin.stat: + path: /tmp/strapi_postgres_latest.sql + register: sql_file + failed_when: not sql_file.stat.exists + + - name: Display SQL file info + ansible.builtin.debug: + msg: "Decompressed SQL file: {{ sql_file.stat.size }} bytes" + + - name: Stop Strapi service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + export DATABASE_PASSWORD=$(cat {{ db_pw_file }}) && \ + docker compose stop web + register: stop_result + changed_when: true + + - name: Display stop result + ansible.builtin.debug: + msg: "Strapi service stopped" + + - name: Wait for PostgreSQL to be ready + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d postgres -c "SELECT 1" 2>&1 + register: db_ready + until: db_ready.rc == 0 + retries: 30 + delay: 2 + changed_when: false + no_log: false + + - name: Terminate existing connections to strapi database + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'strapi' AND pid <> pg_backend_pid();" + register: terminate_result + changed_when: true + failed_when: false + + - name: Drop existing strapi database + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d postgres -c "DROP DATABASE IF EXISTS strapi;" + register: drop_result + changed_when: true + no_log: true + + - name: Create fresh strapi database + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d postgres -c "CREATE DATABASE strapi;" + register: create_result + changed_when: true + no_log: true + + - name: Restore strapi database from backup + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d strapi < /tmp/strapi_postgres_latest.sql + register: restore_result + changed_when: true + no_log: true + + - name: Display restore completion + ansible.builtin.debug: + msg: "Database restored successfully" + + - name: Verify database contains data + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + PGPASSWORD=$(cat {{ db_pw_file }}) docker compose exec -T postgres psql -U strapi -d strapi -c "\dt" 2>&1 + register: table_list + changed_when: false + no_log: false + + - name: Display database tables + ansible.builtin.debug: + msg: "{{ table_list.stdout_lines }}" + + - name: Clean up temporary SQL file + ansible.builtin.file: + path: /tmp/strapi_postgres_latest.sql + state: absent + + - name: Restart Strapi service + ansible.builtin.shell: | + cd {{ deploy_path }} && \ + export DATABASE_PASSWORD=$(cat {{ db_pw_file }}) && \ + docker compose start web + register: start_result + changed_when: true + + - name: Display restart status + ansible.builtin.debug: + msg: "Strapi service restarted successfully" + + - name: Display final status + ansible.builtin.debug: + msg: + - "=====================================" + - "Strapi database restore completed!" + - "=====================================" diff --git a/teardown-demo.sh b/teardown-demo.sh new file mode 100755 index 0000000..b8f2272 --- /dev/null +++ b/teardown-demo.sh @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +INVENTORY="hosts-demo.yaml" + +# Get server IP from inventory or use default +SERVER_IP="${1:-$(grep 'ansible_host:' $INVENTORY | head -1 | awk '{print $2}')}" + +echo -e "${RED}========================================${NC}" +echo -e "${RED}Demo Environment Teardown Script${NC}" +echo -e "${RED}========================================${NC}" +echo "" +echo "Target server: $SERVER_IP" +echo "" + +# Confirm teardown +read -p "Are you sure you want to tear down the demo environment? (yes/no): " confirm +if [ "$confirm" != "yes" ]; then + echo "Teardown cancelled." + exit 0 +fi + +echo "" + +# Function to run SSH command on demo server +run_ssh() { + ssh -o BatchMode=yes ubuntu@$SERVER_IP "$@" +} + +echo -e "${YELLOW}>>> Backing up SSL certificates to S3...${NC}" +if [ -z "$CONTABO_S3_ACCESS_KEY" ] || [ -z "$CONTABO_S3_SECRET_KEY" ]; then + echo -e "${YELLOW}Warning: S3 credentials not set, skipping certificate backup${NC}" + echo "Set CONTABO_S3_ACCESS_KEY and CONTABO_S3_SECRET_KEY to enable backup" +else + S3_BUCKET="${CONTABO_S3_BUCKET:-beoftexas-backup}" + run_ssh "if [ -d /home/ubuntu/deploy/nginx-demo/data/certbot/conf/live ]; then + cd /home/ubuntu/deploy/nginx-demo/data/certbot + sudo tar -czf /tmp/demo-certbot-backup.tar.gz conf + AWS_ACCESS_KEY_ID='$CONTABO_S3_ACCESS_KEY' \ + AWS_SECRET_ACCESS_KEY='$CONTABO_S3_SECRET_KEY' \ + aws s3 cp /tmp/demo-certbot-backup.tar.gz s3://$S3_BUCKET/demo-certbot-backup.tar.gz \ + --endpoint-url https://usc1.contabostorage.com + rm /tmp/demo-certbot-backup.tar.gz + echo 'Certificates backed up to S3' + else + echo 'No certificates to backup' + fi" +fi +echo -e "${GREEN}✓ Certificate backup complete${NC}" +echo "" + +echo -e "${YELLOW}>>> Stopping and removing all demo containers...${NC}" + +# Stop and remove each service +for service in cloudflared nginx-demo beoftexas wbaoftexas strapi; do + echo " Tearing down $service..." + run_ssh "cd /home/ubuntu/deploy/$service 2>/dev/null && sudo docker compose down -v 2>/dev/null || true" +done + +echo -e "${GREEN}✓ All containers stopped and removed${NC}" +echo "" + +echo -e "${YELLOW}>>> Removing Docker volumes...${NC}" +run_ssh "sudo docker volume prune -f 2>/dev/null || true" +echo -e "${GREEN}✓ Volumes removed${NC}" +echo "" + +echo -e "${YELLOW}>>> Removing deploy directories...${NC}" +run_ssh "sudo rm -rf /home/ubuntu/deploy/*" +echo -e "${GREEN}✓ Deploy directories removed${NC}" +echo "" + +echo -e "${YELLOW}>>> Pruning Docker system...${NC}" +run_ssh "sudo docker system prune -af 2>/dev/null || true" +echo -e "${GREEN}✓ Docker system pruned${NC}" +echo "" + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Demo Environment Teardown Complete!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "The demo VM is now clean and ready for a fresh deployment." +echo "" +echo "To redeploy, run:" +echo " export CONTABO_S3_ACCESS_KEY=your_key" +echo " export CONTABO_S3_SECRET_KEY=your_secret" +echo " ./deploy-demo.sh" +echo "" +echo "Note: Cloudflare Tunnel and DNS records are preserved." +echo " SSL certificates backed up to S3 and will be restored on next deploy." +echo "" diff --git a/wbaoftexas/playbook-demo.yaml b/wbaoftexas/playbook-demo.yaml new file mode 100644 index 0000000..172d96c --- /dev/null +++ b/wbaoftexas/playbook-demo.yaml @@ -0,0 +1,60 @@ +- name: Deploy WBA of Texas website via Docker (Demo) + hosts: demo + become: true + tasks: + - name: Log into DockerHub + docker_login: + username: '{{ lookup("env", "DOCKERHUB_USERNAME") }}' + password: '{{ lookup("env", "DOCKERHUB_PASSWORD") }}' + + - name: Creates directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: ubuntu + group: ubuntu + loop: + - "{{ deploy_path }}" + - "{{ deploy_path }}/wbaoftexas" + - "{{ deploy_path }}/wbaoftexas/data" + - "{{ deploy_path }}/wbaoftexas/data/mariadb" + - "{{ deploy_path }}/wbaoftexas/secrets" + + - name: Create shared Docker network + community.docker.docker_network: + name: shared + state: present + + - name: copy Docker Compose files + copy: + src: "{{ item }}" + dest: "{{ deploy_path }}/wbaoftexas/{{ item }}" + owner: ubuntu + group: ubuntu + loop: + - docker-compose.yaml + + - name: copy secrets + copy: + src: "../secret/demo/{{ item }}" + dest: "{{ deploy_path }}/wbaoftexas/secrets/{{ item }}" + owner: ubuntu + group: ubuntu + mode: '0600' + loop: + - wbaoftexas_db_pw.txt + - wbaoftexas_db_root_pw.txt + + - name: Deploy with docker compose + ansible.builtin.shell: + cmd: "export WBAOFTEXAS_DB_PASS=$(cat secrets/wbaoftexas_db_pw.txt) && docker compose up -d" + chdir: "{{ deploy_path }}/wbaoftexas/" + register: compose_up_result + + - name: Restart nginx to pick up wbaoftexas on shared network + ansible.builtin.shell: + cmd: "docker compose restart web" + chdir: "{{ deploy_path }}/nginx-demo" + register: nginx_restart_result + failed_when: false + changed_when: nginx_restart_result.rc == 0