Add Cloudflare automation, ngrok tunnel, fix Dockerfiles
- Ansible cloudflare.yml: DNS, SSL, HSTS, Brotli, bot fight, SEO bots allow - Vault: add cloudflare_zone_id - Workflow: run cloudflare config before deploy - docker-compose-dev: add ngrok tunnel, vault, minio - Ngrok sync script: writes OUTSIDE_URL to .env.local - Fix Dockerfiles: remove mbstring/xml (built-in PHP 8.4), fix libfreetype-dev - Makefile: maintenance_on/off, clear_prod - Playbook: stop_prod, install_prod, start_prod, migrate, clear steps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,5 +20,8 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan 34.90.187.4 >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Configure Cloudflare
|
||||
run: ansible-playbook ansible/cloudflare.yml --vault-password-file <(echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}")
|
||||
|
||||
- name: Deploy
|
||||
run: ansible-playbook -i ansible/hosts.ini ansible/deploy-caddy.yml --vault-password-file <(echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}")
|
||||
|
||||
173
ansible/cloudflare.yml
Normal file
173
ansible/cloudflare.yml
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
- name: Configure Cloudflare for ticket.e-cosplay.fr
|
||||
hosts: localhost
|
||||
connection: local
|
||||
vars_files:
|
||||
- vault.yml
|
||||
|
||||
vars:
|
||||
zone_id: "{{ cloudflare_zone_id }}"
|
||||
cloudflare_record: ticket.e-cosplay.fr
|
||||
server_ip: 34.90.187.4
|
||||
|
||||
tasks:
|
||||
# --- DNS ---
|
||||
- name: Create or update DNS A record
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
type: A
|
||||
name: "{{ cloudflare_record }}"
|
||||
content: "{{ server_ip }}"
|
||||
ttl: 1
|
||||
proxied: true
|
||||
status_code: [200, 409]
|
||||
register: dns_result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Update DNS A record if already exists
|
||||
when: dns_result.status == 409 or (dns_result.json is defined and not dns_result.json.success)
|
||||
block:
|
||||
- name: Get existing DNS record
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records?name={{ cloudflare_record }}&type=A"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
return_content: true
|
||||
register: existing_dns
|
||||
|
||||
- name: Update DNS record
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records/{{ existing_dns.json.result[0].id }}"
|
||||
method: PUT
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
type: A
|
||||
name: "{{ cloudflare_record }}"
|
||||
content: "{{ server_ip }}"
|
||||
ttl: 1
|
||||
proxied: true
|
||||
|
||||
# --- SSL/TLS ---
|
||||
- name: Set SSL mode to Full (Strict)
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/ssl"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: strict
|
||||
|
||||
- name: Enable Always Use HTTPS
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/always_use_https"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: "on"
|
||||
|
||||
- name: Set minimum TLS version to 1.2
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/min_tls_version"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: "1.2"
|
||||
|
||||
# --- Security headers ---
|
||||
- name: Enable HSTS
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/security_header"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value:
|
||||
strict_transport_security:
|
||||
enabled: true
|
||||
max_age: 31536000
|
||||
include_subdomains: true
|
||||
nosniff: true
|
||||
|
||||
# --- Performance ---
|
||||
- name: Enable Brotli compression
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/brotli"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: "on"
|
||||
|
||||
- name: Set browser cache TTL to 1 month
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/browser_cache_ttl"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: 2592000
|
||||
|
||||
# --- Security ---
|
||||
- name: Set security level to medium
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/security_level"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
value: medium
|
||||
|
||||
- name: Enable bot fight mode
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/bot_management"
|
||||
method: PUT
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
fight_mode: true
|
||||
ignore_errors: true
|
||||
|
||||
# --- Allow SEO bots ---
|
||||
- name: Allow Googlebot
|
||||
uri:
|
||||
url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/firewall/rules"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ cloudflare_api_token }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
- filter:
|
||||
expression: '(cf.client.bot) or (http.user_agent contains "Googlebot") or (http.user_agent contains "Bingbot") or (http.user_agent contains "bingbot") or (http.user_agent contains "Yandex") or (http.user_agent contains "DuckDuckBot") or (http.user_agent contains "Baiduspider") or (http.user_agent contains "facebookexternalhit") or (http.user_agent contains "Twitterbot") or (http.user_agent contains "LinkedInBot")'
|
||||
action: allow
|
||||
description: "Allow SEO and social media bots"
|
||||
status_code: [200, 409]
|
||||
ignore_errors: true
|
||||
@@ -1,8 +1,11 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
34376230633964343735383363613430386439326535303762646264333330383166636539643439
|
||||
3663303564386133313965343530383761353837626632390a323831366566356234626166646234
|
||||
64316232613836376264363237346433393931623863653562656164346534663666373364626130
|
||||
3833346535373064660a336234373730383438373233623231363335323162326666346136326162
|
||||
65303265386365656164323838666239303639333534626264333962386631323531656262633363
|
||||
64333734326466356236633061663933663962646165313935633361356339326366613731613765
|
||||
383336626531663034666532636363306130
|
||||
64616263316537643530626465343665623830646361623061333265373065353535643435333632
|
||||
6639663636363630376437323232633662643430643865630a636431653266353930306231383031
|
||||
34393965623762356632633262303632316439333464313161383638366331623833666534653930
|
||||
6435656537306566630a333332663632343030643664626261373536393232666262623466643934
|
||||
35636534656530623865663737336139386137353738623362393933376563323463346432313562
|
||||
36383933306237363936383230303830643030336338353466323933356231343865663663666633
|
||||
34386361303033663366356639353933356466333637653436363261613833373664363861633236
|
||||
65656162643837633336363836663635626164323763323832396236633131393864363064636463
|
||||
61303636346661373362656561356532646364613937663261333939303865326534616237336335
|
||||
3737616162653034666634653736393833356331343430653637
|
||||
|
||||
@@ -90,6 +90,53 @@ services:
|
||||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
|
||||
vault:
|
||||
image: hashicorp/vault:latest
|
||||
container_name: e-ticket_vault
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
environment:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: e-ticket
|
||||
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
|
||||
ports:
|
||||
- "8200:8200"
|
||||
volumes:
|
||||
- vault-data:/vault/file
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: e-ticket_minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: e-ticket
|
||||
MINIO_ROOT_PASSWORD: e-ticket
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
|
||||
ngrok:
|
||||
image: ngrok/ngrok:latest
|
||||
container_name: e-ticket_ngrok
|
||||
command: http caddy:80 --log stdout
|
||||
environment:
|
||||
NGROK_AUTHTOKEN: GXtZtKtRxRF5TFV5pCKD_25f1ALUyQQ9LkyQJgv1dr
|
||||
ports:
|
||||
- "4040:4040"
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
ngrok-sync:
|
||||
image: curlimages/curl:latest
|
||||
container_name: e-ticket_ngrok_sync
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./docker/ngrok/sync.sh:/sync.sh
|
||||
depends_on:
|
||||
- ngrok
|
||||
entrypoint: sh /sync.sh
|
||||
|
||||
redisinsight:
|
||||
image: redis/redisinsight:latest
|
||||
container_name: e-ticket_redisinsight
|
||||
@@ -103,3 +150,5 @@ volumes:
|
||||
db-data:
|
||||
redis-data:
|
||||
bun-modules:
|
||||
vault-data:
|
||||
minio-data:
|
||||
|
||||
29
docker/ngrok/sync.sh
Executable file
29
docker/ngrok/sync.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Waiting for ngrok to start..."
|
||||
sleep 5
|
||||
|
||||
NGROK_URL=""
|
||||
RETRIES=10
|
||||
|
||||
while [ -z "$NGROK_URL" ] && [ "$RETRIES" -gt 0 ]; do
|
||||
NGROK_URL=$(curl -s http://ngrok:4040/api/tunnels | grep -o '"public_url":"https://[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
if [ -z "$NGROK_URL" ]; then
|
||||
echo "Waiting for tunnel..."
|
||||
sleep 2
|
||||
RETRIES=$((RETRIES - 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$NGROK_URL" ]; then
|
||||
echo "ERROR: Could not get ngrok URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch /app/.env.local
|
||||
sed -i '/^OUTSIDE_URL=/d' /app/.env.local
|
||||
echo "OUTSIDE_URL=$NGROK_URL" >> /app/.env.local
|
||||
|
||||
echo "Ngrok URL: $NGROK_URL"
|
||||
echo "Written to .env.local"
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
libfreetype-dev \
|
||||
libmagickwand-dev \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -19,9 +19,7 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
pdo_pgsql \
|
||||
pdo_sqlite \
|
||||
zip \
|
||||
xml \
|
||||
intl \
|
||||
mbstring \
|
||||
gd
|
||||
|
||||
RUN pecl install redis imagick \
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
libfreetype-dev \
|
||||
libmagickwand-dev \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -19,9 +19,7 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
pdo_pgsql \
|
||||
pdo_sqlite \
|
||||
zip \
|
||||
xml \
|
||||
intl \
|
||||
mbstring \
|
||||
gd \
|
||||
opcache
|
||||
|
||||
|
||||
Reference in New Issue
Block a user