diff --git a/ansible/group_vars/wetgit/main.yml b/ansible/group_vars/wetgit/main.yml index fe6ddf3..b7b1ae6 100644 --- a/ansible/group_vars/wetgit/main.yml +++ b/ansible/group_vars/wetgit/main.yml @@ -19,6 +19,7 @@ backend_host: "127.0.0.1" # --- Domains --- server_name: "api.wetgit.nl" forgejo_domain: "git.wetgit.nl" +web_domain: "wetgit.nl" # --- Forgejo --- forgejo_port: 3000 @@ -30,6 +31,17 @@ forgejo_admin_email: coornhert@wetgit.nl redis_port: 6379 redis_host: "127.0.0.1" +# --- Meilisearch --- +meili_port: 7700 +meili_host: "127.0.0.1" +meili_env: "development" +meili_master_key: "{{ vault_meili_master_key | default('') }}" + +# --- Qdrant --- +qdrant_port: 6333 +qdrant_host: "127.0.0.1" +qdrant_api_key: "{{ vault_qdrant_api_key | default('') }}" + # --- Celery --- celery_concurrency: 2 @@ -42,7 +54,16 @@ codeberg_api_token: "{{ vault_codeberg_api_token | default('') }}" # --- AgentMail --- agentmail_api_key: "{{ vault_agentmail_api_key }}" +# --- Mistral AI --- +mistral_api_key: "{{ vault_mistral_api_key }}" + +# --- Local source path (for rsync deploy) --- +local_src_dir: "{{ playbook_dir }}/../" + # --- Secrets (from vault.yml) --- # vault_agentmail_api_key # vault_forgejo_api_token # vault_codeberg_api_token (add when Codeberg account is ready) +# vault_meili_master_key +# vault_qdrant_api_key +# vault_mistral_api_key diff --git a/ansible/group_vars/wetgit/vault.yml b/ansible/group_vars/wetgit/vault.yml index 117dd74..1fd6bea 100644 --- a/ansible/group_vars/wetgit/vault.yml +++ b/ansible/group_vars/wetgit/vault.yml @@ -1,14 +1,22 @@ $ANSIBLE_VAULT;1.1;AES256 -35323237613730303463313335643433616238663932643630636530356461323433666435653436 -3433343462343538333335343165353538613435613962650a656166366364393564353733343561 -66643462313261643538653839393365643634376432373665653133383464313636633762366163 -6562336332396535390a333062323534373963356439353336633964383832313431623934653739 -37646339376338623536323336353931343039323263666265363763373266343533333236346635 -37656436623764393037393138343536313666613439666535656631313031343061346130376136 -64383164643466643162393537343265313632343432336238393030306164636434356463396434 -34656334383731326131393061333138643435366534333965376666393535316334396662633561 -61386636336438383563326565336635643663313934326333323939663637653531363261613733 -38646631333739303737616630663337663265616462346637326539306338613866313762306662 -38633066323936623233336631653836656531633839643739313966623065313931356630613134 -39636539643065663963626437383637643932633164306337626330623466313737623532366631 -6435 +63366162323335343538313162623831383134396331623163663630653637303866633539653338 +6230646432306464636466306161316164313533656430340a663833643334636437313133616434 +62366133353936633734353938323561303334626162383964633734613334373233363138306433 +3865303365356665620a333037376230306632613032303937383931366533346137633766303062 +33373962326562313731313030353936373837626435323265333636623631333432373962653561 +33616435623937313963316531313262346162303961303932383930333831303266393630386635 +66616463643333303834623932313032343638613333373362313439303436333137626638353062 +37353434383764633162373862316535626635353436353735346531366364343138623737383138 +65316363333565336631333333633263643130653965376235333163343335356361643866333661 +35306666373635646238393961356266623732363233646435393939646165623366326130303533 +32326366336337633232656435663230396636353164653563626534613433313437656238666539 +32393630333131376263336136653439393831353662383466346365303532663134623537313531 +38323739376434303261623235393338363938616535363738653631303737373566633763623862 +35666165636132356463366237393263626561343139343833373439383265303438633338323131 +62376364346134346636393330633134363631383234383766363234653565303733653032616230 +36623730376330343331303064383365366338643834663937356262663466353965313936316237 +38643933306163363634373236333761326437636434306565623261316430653565373431303064 +37623864386663613730306431363966323937613961633363343366643864613338326535353232 +31623835663466333336303434303765353233646531626132323933633835353638323038653763 +36663333323762653933633462346561313331633162303033646162643236353233363731613635 +61303164313262613763313231633635626638616366383961646465343163666232 diff --git a/ansible/roles/wetgit-app/tasks/main.yml b/ansible/roles/wetgit-app/tasks/main.yml index cfa8710..4c06614 100644 --- a/ansible/roles/wetgit-app/tasks/main.yml +++ b/ansible/roles/wetgit-app/tasks/main.yml @@ -1,23 +1,86 @@ --- # WetGIT FastAPI application + Celery worker -# Deploys to /opt/wetgit/backend with own venv and systemd services +# Deploys to /opt/wetgit/backend via rsync from local checkout. # # Directories are created by wetgit-forgejo role (runs first). -# This role only manages the FastAPI app and Celery worker. -# -# NOTE: Services are only enabled when application code exists. -# On first deploy (no code yet), this role is effectively a no-op. +# This role syncs source code, installs deps, and manages systemd services. -- name: Check if application code exists +# --- Code deployment via rsync --- +# NOTE: become: no is required on synchronize tasks because rsync +# runs locally and connects to the remote via SSH directly. + +- name: Sync application code to server + ansible.posix.synchronize: + src: "{{ local_src_dir }}/src/" + dest: "{{ app_dir }}/backend/src/" + delete: yes + rsync_opts: + - "--exclude=__pycache__" + - "--exclude=*.pyc" + become: no + notify: restart wetgit + +- name: Sync pyproject.toml + ansible.posix.synchronize: + src: "{{ local_src_dir }}/pyproject.toml" + dest: "{{ app_dir }}/backend/pyproject.toml" + become: no + notify: restart wetgit + +- name: Check if local templates directory exists stat: - path: "{{ app_dir }}/backend/requirements.txt" - register: app_code + path: "{{ local_src_dir }}/templates" + delegate_to: localhost + register: local_templates + become: no + +- name: Sync web templates + ansible.posix.synchronize: + src: "{{ local_src_dir }}/templates/" + dest: "{{ app_dir }}/backend/templates/" + delete: yes + rsync_opts: + - "--exclude=__pycache__" + become: no + when: local_templates.stat.exists + notify: restart wetgit + +- name: Check if local static directory exists + stat: + path: "{{ local_src_dir }}/static" + delegate_to: localhost + register: local_static + become: no + +- name: Sync static assets + ansible.posix.synchronize: + src: "{{ local_src_dir }}/static/" + dest: "{{ app_dir }}/backend/static/" + delete: yes + become: no + when: local_static.stat.exists + +- name: Set backend ownership + file: + path: "{{ app_dir }}/backend" + owner: www-data + group: www-data + recurse: yes + +# --- Python venv and dependencies --- - name: Create Python venv command: python3 -m venv {{ app_dir }}/backend/venv args: creates: "{{ app_dir }}/backend/venv/bin/python" - when: app_code.stat.exists + +- name: Install application with API dependencies + command: "{{ app_dir }}/backend/venv/bin/pip install --upgrade '.[api]'" + args: + chdir: "{{ app_dir }}/backend" + register: pip_install + changed_when: "'Successfully installed' in pip_install.stdout" + notify: restart wetgit - name: Set venv ownership file: @@ -25,14 +88,8 @@ owner: www-data group: www-data recurse: yes - when: app_code.stat.exists -- name: Install Python dependencies - pip: - requirements: "{{ app_dir }}/backend/requirements.txt" - virtualenv: "{{ app_dir }}/backend/venv" - when: app_code.stat.exists - notify: restart wetgit +# --- Configuration --- - name: Deploy environment file template: @@ -43,6 +100,8 @@ mode: "0600" notify: restart wetgit +# --- Systemd services --- + - name: Deploy WetGIT systemd service template: src: wetgit.service.j2 @@ -61,19 +120,18 @@ mode: "0644" notify: restart wetgit-celery -# Only start services when app code is deployed - name: Enable and start WetGIT service systemd: name: wetgit enabled: yes state: started daemon_reload: yes - when: app_code.stat.exists -- name: Enable and start Celery worker +# Celery worker disabled — sync runs via cron, not Celery +# Enable when wetgit.pipeline has a proper Celery app +- name: Disable Celery worker (not yet configured) systemd: name: wetgit-celery - enabled: yes - state: started + enabled: no + state: stopped daemon_reload: yes - when: app_code.stat.exists diff --git a/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 b/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 index f04c628..9c5ea4e 100644 --- a/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 +++ b/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 @@ -9,7 +9,7 @@ User=www-data Group=www-data WorkingDirectory={{ app_dir }}/backend EnvironmentFile={{ app_dir }}/backend/.env -ExecStart={{ app_dir }}/backend/venv/bin/celery -A tasks worker --loglevel=info --concurrency={{ celery_concurrency }} +ExecStart={{ app_dir }}/backend/venv/bin/celery -A wetgit.tasks worker --loglevel=info --concurrency={{ celery_concurrency }} Restart=always RestartSec=10 diff --git a/ansible/roles/wetgit-app/templates/wetgit.env.j2 b/ansible/roles/wetgit-app/templates/wetgit.env.j2 index e59264b..e3e376d 100644 --- a/ansible/roles/wetgit-app/templates/wetgit.env.j2 +++ b/ansible/roles/wetgit-app/templates/wetgit.env.j2 @@ -11,9 +11,28 @@ REDIS_URL=redis://{{ redis_host }}:{{ redis_port }}/0 CELERY_BROKER_URL=redis://{{ redis_host }}:{{ redis_port }}/0 CELERY_RESULT_BACKEND=redis://{{ redis_host }}:{{ redis_port }}/1 +# Meilisearch +MEILI_URL=http://{{ meili_host }}:{{ meili_port }} +{% if meili_master_key | length > 0 %} +MEILI_MASTER_KEY={{ meili_master_key }} +{% endif %} + +# Qdrant +QDRANT_URL=http://{{ qdrant_host }}:{{ qdrant_port }} +{% if qdrant_api_key | length > 0 %} +QDRANT_API_KEY={{ qdrant_api_key }} +{% endif %} + +# Mistral AI +MISTRAL_API_KEY={{ mistral_api_key }} + # AgentMail AGENTMAIL_API_KEY={{ agentmail_api_key }} +# Forgejo +FORGEJO_URL=https://{{ forgejo_domain }} +FORGEJO_API_TOKEN={{ forgejo_api_token }} + # Data WETGIT_DATA_DIR={{ data_dir }} -WETGIT_GIT_REPOS_DIR={{ data_dir }}/git-repos +WETGIT_GIT_REPOS_DIR={{ app_dir }}/app diff --git a/ansible/roles/wetgit-app/templates/wetgit.service.j2 b/ansible/roles/wetgit-app/templates/wetgit.service.j2 index 9e83dba..fb87a44 100644 --- a/ansible/roles/wetgit-app/templates/wetgit.service.j2 +++ b/ansible/roles/wetgit-app/templates/wetgit.service.j2 @@ -9,7 +9,7 @@ User=www-data Group=www-data WorkingDirectory={{ app_dir }}/backend EnvironmentFile={{ app_dir }}/backend/.env -ExecStart={{ app_dir }}/backend/venv/bin/uvicorn main:app --host {{ backend_host }} --port {{ backend_port }} --workers {{ backend_workers }} +ExecStart={{ app_dir }}/backend/venv/bin/uvicorn wetgit.api.app:app --host {{ backend_host }} --port {{ backend_port }} --workers {{ backend_workers }} Restart=always RestartSec=5 diff --git a/ansible/roles/wetgit-forgejo/handlers/main.yml b/ansible/roles/wetgit-forgejo/handlers/main.yml index 90d076a..e82150a 100644 --- a/ansible/roles/wetgit-forgejo/handlers/main.yml +++ b/ansible/roles/wetgit-forgejo/handlers/main.yml @@ -1,5 +1,12 @@ --- - name: restart forgejo + community.docker.docker_compose_v2: + project_src: "{{ app_dir }}/docker" + services: + - forgejo + state: restarted + +- name: restart docker stack community.docker.docker_compose_v2: project_src: "{{ app_dir }}/docker" state: restarted diff --git a/ansible/roles/wetgit-forgejo/tasks/main.yml b/ansible/roles/wetgit-forgejo/tasks/main.yml index fee8a5f..46438b9 100644 --- a/ansible/roles/wetgit-forgejo/tasks/main.yml +++ b/ansible/roles/wetgit-forgejo/tasks/main.yml @@ -45,19 +45,22 @@ group: "{{ item.group }}" mode: "0755" loop: + # Parents first (owned by root) + - { path: "{{ app_dir }}", owner: root, group: root } + - { path: "{{ data_dir }}", owner: root, group: root } # Forgejo directories (owned by wetgit user) - { path: "{{ app_dir }}/docker", owner: wetgit, group: wetgit } - { path: "{{ forgejo_data_dir }}", owner: wetgit, group: wetgit } - { path: "{{ forgejo_data_dir }}/gitea/conf", owner: wetgit, group: wetgit } - { path: "{{ data_dir }}/redis", owner: wetgit, group: wetgit } + - { path: "{{ data_dir }}/meilisearch", owner: wetgit, group: wetgit } + - { path: "{{ data_dir }}/qdrant", owner: wetgit, group: wetgit } - { path: "{{ app_dir }}/scripts", owner: wetgit, group: wetgit } - { path: "{{ app_dir }}/backups", owner: wetgit, group: wetgit } - { path: "{{ app_dir }}/logs", owner: wetgit, group: wetgit } - { path: "{{ app_dir }}/mirrors", owner: wetgit, group: wetgit } # Application directories (owned by www-data for FastAPI/Celery) - - { path: "{{ app_dir }}", owner: root, group: root } - { path: "{{ app_dir }}/backend", owner: www-data, group: www-data } - - { path: "{{ data_dir }}", owner: root, group: root } - { path: "{{ data_dir }}/git-repos", owner: www-data, group: www-data } # --- Forgejo config --- @@ -81,8 +84,8 @@ dest: "{{ app_dir }}/docker/docker-compose.yml" owner: wetgit group: wetgit - mode: "0644" - notify: restart forgejo + mode: "0640" + notify: restart docker stack - name: Start WetGIT Docker stack community.docker.docker_compose_v2: @@ -141,7 +144,7 @@ - name: Configure backup cron (weekly Sunday 02:00) cron: name: "wetgit-backup" - user: root + user: wetgit weekday: "0" hour: "2" minute: "0" @@ -164,3 +167,16 @@ hour: "5" minute: "0" job: "find {{ app_dir }}/logs -name '*.log' -mtime +30 -delete" + +# --- IPv4 preference (Hetzner IPv6 causes timeouts to external APIs) --- +# TODO: migrate to dt-platform's server role when appropriate + +- name: Ensure IPv4 precedence in gai.conf + lineinfile: + path: /etc/gai.conf + regexp: '^precedence\s+::ffff:0:0/96' + line: "precedence ::ffff:0:0/96 100" + create: yes + owner: root + group: root + mode: "0644" diff --git a/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 b/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 index 22917dc..7751d00 100644 --- a/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 +++ b/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 @@ -40,6 +40,64 @@ services: networks: - wetgit + meilisearch: + image: getmeili/meilisearch:v1.12 + container_name: wetgit-meilisearch + restart: unless-stopped + ports: + - "{{ backend_host }}:{{ meili_port }}:7700" + volumes: + - {{ data_dir }}/meilisearch:/meili_data + environment: + - MEILI_ENV={{ meili_env }} +{% if meili_master_key | length > 0 %} + - MEILI_MASTER_KEY={{ meili_master_key }} +{% endif %} + - MEILI_LOG_LEVEL=WARN + deploy: + resources: + limits: + memory: 1G + cpus: "2.0" + reservations: + memory: 256M + cpus: "0.5" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - wetgit + + qdrant: + image: qdrant/qdrant:v1.13.2 + container_name: wetgit-qdrant + restart: unless-stopped + ports: + - "{{ backend_host }}:{{ qdrant_port }}:6333" + volumes: + - {{ data_dir }}/qdrant:/qdrant/storage +{% if qdrant_api_key | length > 0 %} + environment: + - QDRANT__SERVICE__API_KEY={{ qdrant_api_key }} +{% endif %} + deploy: + resources: + limits: + memory: 512M + cpus: "1.0" + reservations: + memory: 128M + cpus: "0.25" + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - wetgit + networks: wetgit: name: wetgit-network diff --git a/ansible/roles/wetgit-nginx/tasks/main.yml b/ansible/roles/wetgit-nginx/tasks/main.yml index ce9035f..08862b3 100644 --- a/ansible/roles/wetgit-nginx/tasks/main.yml +++ b/ansible/roles/wetgit-nginx/tasks/main.yml @@ -3,7 +3,11 @@ # IMPORTANT: Only adds vhost configs. Does NOT touch global nginx.conf # (managed by dt-platform's nginx role). # -# Strategy: Deploy HTTP-only first → get SSL certs → deploy full HTTPS config. +# Strategy: +# 1. Check if SSL certs exist +# 2. If no cert: deploy HTTP-only config → certbot → deploy HTTPS +# 3. If cert exists: deploy HTTPS config directly +# 4. Enable vhosts (symlinks) after config files exist # --- Step 1: Check existing SSL certificates --- @@ -17,7 +21,12 @@ path: "/etc/letsencrypt/live/{{ forgejo_domain }}/fullchain.pem" register: ssl_cert_git -# --- Step 2: Deploy HTTP-only configs for domains without certs --- +- name: Check if Web SSL certificate exists + stat: + path: "/etc/letsencrypt/live/{{ web_domain }}/fullchain.pem" + register: ssl_cert_web + +# --- Step 2: Deploy HTTP-only configs for domains that need new certs --- - name: Deploy API HTTP-only vhost (pre-SSL) copy: @@ -55,23 +64,51 @@ when: not ssl_cert_git.stat.exists notify: reload nginx -# --- Step 3: Enable vhosts and reload nginx --- +- name: Deploy Web HTTP-only vhost (pre-SSL) + copy: + content: | + # Temporary HTTP-only config for SSL provisioning — managed by Ansible + server { + listen 80; + listen [::]:80; + server_name {{ web_domain }}; + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { return 503; } + } + dest: /etc/nginx/sites-available/wetgit-web.conf + owner: root + group: root + mode: "0644" + when: not ssl_cert_web.stat.exists + notify: reload nginx -- name: Enable API vhost +# --- Step 3: Enable vhosts that need new certs + reload for certbot --- + +- name: Enable API vhost (pre-SSL) file: src: /etc/nginx/sites-available/wetgit-api.conf dest: /etc/nginx/sites-enabled/wetgit-api.conf state: link + when: not ssl_cert_api.stat.exists notify: reload nginx -- name: Enable Forgejo vhost +- name: Enable Forgejo vhost (pre-SSL) file: src: /etc/nginx/sites-available/wetgit-git.conf dest: /etc/nginx/sites-enabled/wetgit-git.conf state: link + when: not ssl_cert_git.stat.exists notify: reload nginx -# Force handler to run now so nginx has the HTTP configs before certbot +- name: Enable Web vhost (pre-SSL) + file: + src: /etc/nginx/sites-available/wetgit-web.conf + dest: /etc/nginx/sites-enabled/wetgit-web.conf + state: link + when: not ssl_cert_web.stat.exists + notify: reload nginx + +# Force handler to run so nginx has the HTTP configs before certbot - name: Flush handlers (reload nginx for certbot) meta: flush_handlers @@ -97,7 +134,34 @@ when: not ssl_cert_git.stat.exists register: certbot_git -# --- Step 5: Deploy full HTTPS configs --- +- name: Obtain SSL certificate for {{ web_domain }} + command: > + certbot certonly --webroot + -w /var/www/certbot + -d {{ web_domain }} + --non-interactive --agree-tos + --email coornhert@wetgit.nl + when: not ssl_cert_web.stat.exists + register: certbot_web + +# --- Step 5: Re-check SSL certs after certbot --- + +- name: Re-check API SSL certificate + stat: + path: "/etc/letsencrypt/live/{{ server_name }}/fullchain.pem" + register: ssl_cert_api_final + +- name: Re-check Forgejo SSL certificate + stat: + path: "/etc/letsencrypt/live/{{ forgejo_domain }}/fullchain.pem" + register: ssl_cert_git_final + +- name: Re-check Web SSL certificate + stat: + path: "/etc/letsencrypt/live/{{ web_domain }}/fullchain.pem" + register: ssl_cert_web_final + +# --- Step 6: Deploy full HTTPS configs + enable vhosts --- - name: Deploy API nginx vhost (full HTTPS) template: @@ -106,6 +170,7 @@ owner: root group: root mode: "0644" + when: ssl_cert_api_final.stat.exists notify: reload nginx - name: Deploy Forgejo nginx vhost (full HTTPS) @@ -115,4 +180,37 @@ owner: root group: root mode: "0644" + when: ssl_cert_git_final.stat.exists + notify: reload nginx + +- name: Deploy Web nginx vhost (full HTTPS) + template: + src: wetgit-web.conf.j2 + dest: /etc/nginx/sites-available/wetgit-web.conf + owner: root + group: root + mode: "0644" + when: ssl_cert_web_final.stat.exists + notify: reload nginx + +# Enable all vhosts (idempotent — creates symlink if not exists) +- name: Enable API vhost + file: + src: /etc/nginx/sites-available/wetgit-api.conf + dest: /etc/nginx/sites-enabled/wetgit-api.conf + state: link + notify: reload nginx + +- name: Enable Forgejo vhost + file: + src: /etc/nginx/sites-available/wetgit-git.conf + dest: /etc/nginx/sites-enabled/wetgit-git.conf + state: link + notify: reload nginx + +- name: Enable Web vhost + file: + src: /etc/nginx/sites-available/wetgit-web.conf + dest: /etc/nginx/sites-enabled/wetgit-web.conf + state: link notify: reload nginx diff --git a/ansible/roles/wetgit-nginx/templates/wetgit-web.conf.j2 b/ansible/roles/wetgit-nginx/templates/wetgit-web.conf.j2 new file mode 100644 index 0000000..48cbb4e --- /dev/null +++ b/ansible/roles/wetgit-nginx/templates/wetgit-web.conf.j2 @@ -0,0 +1,51 @@ +# WetGIT frontend (wetgit.nl) — managed by WetGIT Ansible (not dt-platform) +# Do NOT edit manually + +server { + listen 80; + listen [::]:80; + server_name {{ web_domain }}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ web_domain }}; + + ssl_certificate /etc/letsencrypt/live/{{ web_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ web_domain }}/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Frontend proxy (same FastAPI app serves the web UI) + location / { + proxy_pass http://{{ backend_host }}:{{ backend_port }}; + 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; + + proxy_read_timeout 120s; + proxy_connect_timeout 10s; + } + + # Static assets (served by FastAPI/Starlette) + location /static/ { + proxy_pass http://{{ backend_host }}:{{ backend_port }}/static/; + proxy_set_header Host $host; + expires 1d; + add_header Cache-Control "public"; + } +}