diff --git a/playbooks/deploy_neurapress_docker.yaml b/playbooks/deploy_neurapress_docker.yaml new file mode 100644 index 0000000..b8b89dc --- /dev/null +++ b/playbooks/deploy_neurapress_docker.yaml @@ -0,0 +1,11 @@ +- name: setup neurapress + hosts: all + become: true + vars: + neurapress_domain: "{{ domain }}" + neurapress_workspace: /opt/neurapress + neurapress_image: neurapress:prod + neurapress_certbot_email: manbuzhe2009@qq.com + roles: + - vhosts/docker/ + - docker/neurapress/ diff --git a/playbooks/roles/docker/neurapress/defaults/main.yml b/playbooks/roles/docker/neurapress/defaults/main.yml new file mode 100644 index 0000000..51a5116 --- /dev/null +++ b/playbooks/roles/docker/neurapress/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# Default deployment directory for Neurapress Docker stack +neurapress_deploy_dir: /opt/neurapress +neurapress_workspace: "{{ neurapress_deploy_dir }}" +neurapress_domain: write.svc.plus +neurapress_image: neurapress:prod +neurapress_certbot_email: manbuzhe2009@qq.com diff --git a/playbooks/roles/docker/neurapress/files/nginx/nginx.conf b/playbooks/roles/docker/neurapress/files/nginx/nginx.conf new file mode 100644 index 0000000..a228fd2 --- /dev/null +++ b/playbooks/roles/docker/neurapress/files/nginx/nginx.conf @@ -0,0 +1,51 @@ +user nginx; +worker_processes auto; + +# Logs → container stdout / stderr +error_log /dev/stderr warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Access log → stdout + log_format main + '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"'; + + access_log /dev/stdout main; + + # Core performance (safe defaults) + sendfile on; + tcp_nodelay on; + keepalive_timeout 65; + server_tokens off; + + # TLS session cache (in-memory only) + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Gzip (lightweight) + gzip on; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain + text/css + application/json + application/javascript + application/xml + image/svg+xml; + + # Allow uploads (Markdown / images) + client_max_body_size 50m; + + # Load virtual hosts + include /etc/nginx/conf.d/*.conf; +} diff --git a/playbooks/roles/docker/neurapress/files/run.sh b/playbooks/roles/docker/neurapress/files/run.sh new file mode 100644 index 0000000..d915823 --- /dev/null +++ b/playbooks/roles/docker/neurapress/files/run.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Helper script to start the Neurapress docker compose stack +cd "$(dirname "$0")" +docker compose -f docker-compose.yaml up -d diff --git a/playbooks/roles/docker/neurapress/tasks/main.yml b/playbooks/roles/docker/neurapress/tasks/main.yml new file mode 100644 index 0000000..300f6d1 --- /dev/null +++ b/playbooks/roles/docker/neurapress/tasks/main.yml @@ -0,0 +1,69 @@ +--- +- name: Ensure Neurapress directories exist + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ neurapress_workspace }}" + - "{{ neurapress_workspace }}/certbot" + - "{{ neurapress_workspace }}/certbot/conf" + - "{{ neurapress_workspace }}/certbot/www" + - "{{ neurapress_workspace }}/nginx" + - "{{ neurapress_workspace }}/nginx/conf.d" + +- name: Ensure Neurapress workspace ownership + become: true + ansible.builtin.file: + path: "{{ neurapress_workspace }}" + state: directory + recurse: true + owner: "1000" + group: "1000" + mode: "0755" + +- name: Template Neurapress configuration files + become: true + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ neurapress_workspace }}/{{ item.dest }}" + mode: "{{ item.mode | default('0644') }}" + loop: + - { src: 'docker-compose.yaml', dest: 'docker-compose.yaml' } + - { src: 'nginx/conf.d/default.conf', dest: 'nginx/conf.d/default.conf' } + - { src: 'nginx/conf.d/bootstrap-nginx.conf', dest: 'nginx/conf.d/bootstrap-nginx.conf' } + +- name: Copy Neurapress static files + become: true + ansible.builtin.copy: + src: "{{ item.src }}" + dest: "{{ neurapress_workspace }}/{{ item.dest }}" + mode: "{{ item.mode | default('0644') }}" + loop: + - { src: 'run.sh', dest: 'run.sh', mode: '0755' } + - { src: 'nginx/nginx.conf', dest: 'nginx/nginx.conf' } + +- name: Bootstrap NGINX (80-only for ACME) + become: true + command: docker compose --profile bootstrap -f {{ neurapress_workspace }}/docker-compose.yaml up -d bootstrap-nginx + args: + chdir: "{{ neurapress_workspace }}" + +- name: Run certbot initial ACME challenge + become: true + command: docker compose --profile bootstrap -f {{ neurapress_workspace }}/docker-compose.yaml run --rm certbot + args: + chdir: "{{ neurapress_workspace }}" + +- name: Destroy Bootstrap NGINX (80-only for ACME) + become: true + command: docker compose --profile bootstrap -f {{ neurapress_workspace }}/docker-compose.yaml down bootstrap-nginx + args: + chdir: "{{ neurapress_workspace }}" + +- name: Bring up Neurapress stack + become: true + command: docker compose -f {{ neurapress_workspace }}/docker-compose.yaml up -d + args: + chdir: "{{ neurapress_workspace }}" diff --git a/playbooks/roles/docker/neurapress/templates/docker-compose.yaml b/playbooks/roles/docker/neurapress/templates/docker-compose.yaml new file mode 100644 index 0000000..96fc210 --- /dev/null +++ b/playbooks/roles/docker/neurapress/templates/docker-compose.yaml @@ -0,0 +1,68 @@ +services: + app: + image: "{{ neurapress_image }}" + command: pnpm start + ports: + - "3000:3000" + environment: + - NODE_ENV=production + networks: + - app + + nginx: + image: nginx:mainline-alpine + container_name: neurapress-nginx + depends_on: + - app + ports: + - "80:80" + - "443:443" + volumes: + - "{{ neurapress_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro" + - "{{ neurapress_workspace }}/nginx/conf.d:/etc/nginx/conf.d:ro" + - "{{ neurapress_workspace }}/certbot/conf:/etc/letsencrypt" + - "{{ neurapress_workspace }}/certbot/www:/var/www/certbot" + networks: + - app + + bootstrap-nginx: + profiles: ["bootstrap"] + image: nginx:mainline-alpine + container_name: bootstrap-nginx + volumes: + - "{{ neurapress_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro" + - "{{ neurapress_workspace }}/nginx/conf.d/bootstrap-nginx.conf:/etc/nginx/conf.d/default.conf:ro" + - "{{ neurapress_workspace }}/certbot/conf:/etc/letsencrypt" + - "{{ neurapress_workspace }}/certbot/www:/var/www/certbot" + ports: + - "80:80" + networks: + - app + healthcheck: + test: ["CMD", "wget", "-qO-", "http://{{ neurapress_domain }}"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 3s + + certbot: + profiles: ["bootstrap"] + image: certbot/certbot + container_name: certbot + command: > + certonly --webroot + --webroot-path=/var/www/certbot + --email {{ neurapress_certbot_email }} + --agree-tos + --no-eff-email + --keep-until-expiring + --non-interactive + -d {{ neurapress_domain }} + volumes: + - "{{ neurapress_workspace }}/certbot/conf:/etc/letsencrypt" + - "{{ neurapress_workspace }}/certbot/www:/var/www/certbot" + networks: + - app + +networks: + app: diff --git a/playbooks/roles/docker/neurapress/templates/nginx/conf.d/bootstrap-nginx.conf b/playbooks/roles/docker/neurapress/templates/nginx/conf.d/bootstrap-nginx.conf new file mode 100644 index 0000000..58baf72 --- /dev/null +++ b/playbooks/roles/docker/neurapress/templates/nginx/conf.d/bootstrap-nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name {{ neurapress_domain }}; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # 不 redirect!不要 https! + # certbot 需要纯 http 验证 +} diff --git a/playbooks/roles/docker/neurapress/templates/nginx/conf.d/default.conf b/playbooks/roles/docker/neurapress/templates/nginx/conf.d/default.conf new file mode 100644 index 0000000..3af97ed --- /dev/null +++ b/playbooks/roles/docker/neurapress/templates/nginx/conf.d/default.conf @@ -0,0 +1,49 @@ +# ---------------------------------------------------- +# 80 - ACME Challenge + Redirect to HTTPS +# ---------------------------------------------------- +server { + listen 80; + server_name {{ neurapress_domain }}; + + # Certbot HTTP-01 challenge + location ^~ /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # All HTTP → HTTPS + location / { + return 301 https://$host$request_uri; + } +} + +# ---------------------------------------------------- +# 443 - TLS Termination for Neurapress +# ---------------------------------------------------- +server { + listen 443 ssl http2; + server_name {{ neurapress_domain }}; + + ssl_certificate /etc/letsencrypt/live/{{ neurapress_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ neurapress_domain }}/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # Next.js / Neurapress + location / { + proxy_pass http://app:3000; + proxy_http_version 1.1; + + # WebSocket / HMR support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard headers + 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 https; + + proxy_read_timeout 300; + } +}